// Copyright 2015 Matthew Holt // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package certmagic import ( "encoding/json" "fmt" "log" "path/filepath" "github.com/go-acme/lego/challenge" "github.com/go-acme/lego/challenge/tlsalpn01" ) // tlsALPNSolver is a type that can solve TLS-ALPN challenges using // an existing listener and our custom, in-memory certificate cache. type tlsALPNSolver struct { certCache *Cache } // Present adds the challenge certificate to the cache. func (s tlsALPNSolver) Present(domain, token, keyAuth string) error { cert, err := tlsalpn01.ChallengeCert(domain, keyAuth) if err != nil { return err } certHash := hashCertificateChain(cert.Certificate) s.certCache.mu.Lock() s.certCache.cache[tlsALPNCertKeyName(domain)] = Certificate{ Certificate: *cert, Names: []string{domain}, Hash: certHash, // perhaps not necesssary } s.certCache.mu.Unlock() return nil } // CleanUp removes the challenge certificate from the cache. func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error { s.certCache.mu.Lock() delete(s.certCache.cache, tlsALPNCertKeyName(domain)) s.certCache.mu.Unlock() return nil } // tlsALPNCertKeyName returns the key to use when caching a cert // for use with the TLS-ALPN ACME challenge. It is simply to help // avoid conflicts (although at time of writing, there shouldn't // be, since the cert cache is keyed by hash of certificate chain). func tlsALPNCertKeyName(sniName string) string { return sniName + ":acme-tls-alpn" } // distributedSolver allows the ACME HTTP-01 and TLS-ALPN challenges // to be solved by an instance other than the one which initiated it. // This is useful behind load balancers or in other cluster/fleet // configurations. The only requirement is that the instance which // initiates the challenge shares the same storage and locker with // the others in the cluster. The storage backing the certificate // cache in distributedSolver.config is crucial. // // Obviously, the instance which completes the challenge must be // serving on the HTTPChallengePort for the HTTP-01 challenge or the // TLSALPNChallengePort for the TLS-ALPN-01 challenge (or have all // the packets port-forwarded) to receive and handle the request. The // server which receives the challenge must handle it by checking to // see if the challenge token exists in storage, and if so, decode it // and use it to serve up the correct response. HTTPChallengeHandler // in this package as well as the GetCertificate method implemented // by a Config support and even require this behavior. // // In short: the only two requirements for cluster operation are // sharing sync and storage, and using the facilities provided by // this package for solving the challenges. type distributedSolver struct { // The config with a certificate cache // with a reference to the storage to // use which is shared among all the // instances in the cluster - REQUIRED. config *Config // Since the distributedSolver is only a // wrapper over an actual solver, place // the actual solver here. providerServer challenge.Provider } // Present invokes the underlying solver's Present method // and also stores domain, token, and keyAuth to the storage // backing the certificate cache of dhs.config. func (dhs distributedSolver) Present(domain, token, keyAuth string) error { if dhs.providerServer != nil { err := dhs.providerServer.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("presenting with standard provider server: %v", err) } } infoBytes, err := json.Marshal(challengeInfo{ Domain: domain, Token: token, KeyAuth: keyAuth, }) if err != nil { return err } return dhs.config.Storage.Store(dhs.challengeTokensKey(domain), infoBytes) } // CleanUp invokes the underlying solver's CleanUp method // and also cleans up any assets saved to storage. func (dhs distributedSolver) CleanUp(domain, token, keyAuth string) error { if dhs.providerServer != nil { err := dhs.providerServer.CleanUp(domain, token, keyAuth) if err != nil { log.Printf("[ERROR] Cleaning up standard provider server: %v", err) } } return dhs.config.Storage.Delete(dhs.challengeTokensKey(domain)) } // challengeTokensPrefix returns the key prefix for challenge info. func (dhs distributedSolver) challengeTokensPrefix() string { return filepath.Join(StorageKeys.CAPrefix(dhs.config.CA), "challenge_tokens") } // challengeTokensKey returns the key to use to store and access // challenge info for domain. func (dhs distributedSolver) challengeTokensKey(domain string) string { return filepath.Join(dhs.challengeTokensPrefix(), StorageKeys.Safe(domain)+".json") } type challengeInfo struct { Domain, Token, KeyAuth string }