@@ -24,7 +24,9 @@ import (
"fmt"
"io"
mathrand "math/rand"
"net"
"net/http"
"path"
"strconv"
"strings"
"sync"
@@ -80,8 +82,9 @@ func defaultHostPolicy(context.Context, string) error {
}
// Manager is a stateful certificate manager built on top of acme.Client.
// It obtains and refreshes certificates automatically,
// as well as providing them to a TLS server via tls.Config.
// It obtains and refreshes certificates automatically using "tls-sni-01",
// "tls-sni-02" and "http-01" challenge types, as well as providing them
// to a TLS server via tls.Config.
//
// You must specify a cache implementation, such as DirCache,
// to reuse obtained certificates across program restarts.
@@ -150,15 +153,26 @@ type Manager struct {
stateMu sync.Mutex
state map [string ]* certState // keyed by domain name
// tokenCert is keyed by token domain name, which matches server name
// of ClientHello. Keys always have ".acme.invalid" suffix.
tokenCertMu sync.RWMutex
tokenCert map [string ]* tls.Certificate
// renewal tracks the set of domains currently running renewal timers.
// It is keyed by domain name.
renewalMu sync.Mutex
renewal map [string ]* domainRenewal
// tokensMu guards the rest of the fields: tryHTTP01, certTokens and httpTokens.
tokensMu sync.RWMutex
// tryHTTP01 indicates whether the Manager should try "http-01" challenge type
// during the authorization flow.
tryHTTP01 bool
// httpTokens contains response body values for http-01 challenges
// and is keyed by the URL path at which a challenge response is expected
// to be provisioned.
// The entries are stored for the duration of the authorization flow.
httpTokens map [string ][]byte
// certTokens contains temporary certificates for tls-sni challenges
// and is keyed by token domain name, which matches server name of ClientHello.
// Keys always have ".acme.invalid" suffix.
// The entries are stored for the duration of the authorization flow.
certTokens map [string ]* tls.Certificate
}
// GetCertificate implements the tls.Config.GetCertificate hook.
@@ -185,14 +199,16 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
return nil , errors .New ("acme/autocert: server name contains invalid character" )
}
// In the worst-case scenario, the timeout needs to account for caching, host policy,
// domain ownership verification and certificate issuance.
ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Minute )
defer cancel ()
// check whether this is a token cert requested for TLS-SNI challenge
if strings .HasSuffix (name , ".acme.invalid" ) {
m .tokenCertMu .RLock ()
defer m .tokenCertMu .RUnlock ()
if cert := m .tokenCert [name ]; cert != nil {
m .tokensMu .RLock ()
defer m .tokensMu .RUnlock ()
if cert := m .certTokens [name ]; cert != nil {
return cert , nil
}
if cert , err := m .cacheGet (ctx , name ); err == nil {
@@ -224,6 +240,68 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
return cert , nil
}
// HTTPHandler configures the Manager to provision ACME "http-01" challenge responses.
// It returns an http.Handler that responds to the challenges and must be
// running on port 80. If it receives a request that is not an ACME challenge,
// it delegates the request to the optional fallback handler.
//
// If fallback is nil, the returned handler redirects all GET and HEAD requests
// to the default TLS port 443 with 302 Found status code, preserving the original
// request path and query. It responds with 400 Bad Request to all other HTTP methods.
// The fallback is not protected by the optional HostPolicy.
//
// Because the fallback handler is run with unencrypted port 80 requests,
// the fallback should not serve TLS-only requests.
//
// If HTTPHandler is never called, the Manager will only use TLS SNI
// challenges for domain verification.
func (m * Manager ) HTTPHandler (fallback http.Handler ) http.Handler {
m .tokensMu .Lock ()
defer m .tokensMu .Unlock ()
m .tryHTTP01 = true
if fallback == nil {
fallback = http .HandlerFunc (handleHTTPRedirect )
}
return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
if ! strings .HasPrefix (r .URL .Path , "/.well-known/acme-challenge/" ) {
fallback .ServeHTTP (w , r )
return
}
// A reasonable context timeout for cache and host policy only,
// because we don't wait for a new certificate issuance here.
ctx , cancel := context .WithTimeout (r .Context (), time .Minute )
defer cancel ()
if err := m .hostPolicy ()(ctx , r .Host ); err != nil {
http .Error (w , err .Error (), http .StatusForbidden )
return
}
data , err := m .httpToken (ctx , r .URL .Path )
if err != nil {
http .Error (w , err .Error (), http .StatusNotFound )
return
}
w .Write (data )
})
}
func handleHTTPRedirect (w http.ResponseWriter , r * http.Request ) {
if r .Method != "GET" && r .Method != "HEAD" {
http .Error (w , "Use HTTPS" , http .StatusBadRequest )
return
}
target := "https://" + stripPort (r .Host ) + r .URL .RequestURI ()
http .Redirect (w , r , target , http .StatusFound )
}
func stripPort (hostport string ) string {
host , _ , err := net .SplitHostPort (hostport )
if err != nil {
return hostport
}
return net .JoinHostPort (host , "443" )
}
// cert returns an existing certificate either from m.state or cache.
// If a certificate is found in cache but not in m.state, the latter will be filled
// with the cached value.
@@ -442,13 +520,14 @@ func (m *Manager) certState(domain string) (*certState, error) {
// authorizedCert starts the domain ownership verification process and requests a new cert upon success.
// The key argument is the certificate private key.
func (m * Manager ) authorizedCert (ctx context.Context , key crypto.Signer , domain string ) (der [][]byte , leaf * x509.Certificate , err error ) {
if err := m .verify (ctx , domain ); err != nil {
return nil , nil , err
}
client , err := m .acmeClient (ctx )
if err != nil {
return nil , nil , err
}
if err := m .verify (ctx , client , domain ); err != nil {
return nil , nil , err
}
csr , err := certRequest (key , domain )
if err != nil {
return nil , nil , err
@@ -464,98 +543,171 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain
return der , leaf , nil
}
// verify starts a new identifier (domain) authorization flow.
// It prepares a challenge response and then blocks until the authorization
// is marked as "completed" by the CA (either succeeded or failed).
//
// verify returns nil iff the verification was successful.
func (m * Manager ) verify (ctx context.Context , domain string ) error {
client , err := m .acmeClient (ctx )
if err != nil {
return err
}
// start domain authorization and get the challenge
authz , err := client .Authorize (ctx , domain )
if err != nil {
return err
}
// maybe don't need to at all
if authz .Status == acme .StatusValid {
return nil
}
// verify runs the identifier (domain) authorization flow
// using each applicable ACME challenge type.
func (m * Manager ) verify (ctx context.Context , client * acme.Client , domain string ) error {
// The list of challenge types we'll try to fulfill
// in this specific order.
challengeTypes := []string {"tls-sni-02" , "tls-sni-01" }
m .tokensMu .RLock ()
if m .tryHTTP01 {
challengeTypes = append (challengeTypes , "http-01" )
}
m .tokensMu .RUnlock ()
var nextTyp int // challengeType index of the next challenge type to try
for {
// Start domain authorization and get the challenge.
authz , err := client .Authorize (ctx , domain )
if err != nil {
return err
}
// No point in accepting challenges if the authorization status
// is in a final state.
switch authz .Status {
case acme .StatusValid :
return nil // already authorized
case acme .StatusInvalid :
return fmt .Errorf ("acme/autocert: invalid authorization %q" , authz .URI )
}
// pick a challenge: prefer tls-sni-02 over tls-sni-01
// TODO: consider authz.Combinations
var chal * acme.Challenge
for _ , c := range authz .Challenges {
if c .Type == "tls-sni-02" {
chal = c
break
// Pick the next preferred challenge.
var chal * acme.Challenge
for chal == nil && nextTyp < len (challengeTypes ) {
chal = pickChallenge (challengeTypes [nextTyp ], authz .Challenges )
nextTyp ++
}
if c .Type == "tls-sni-01" {
chal = c
if chal == nil {
return fmt .Errorf ("acme/autocert: unable to authorize %q; tried %q" , domain , challengeTypes )
}
cleanup , err := m .fulfill (ctx , client , chal )
if err != nil {
continue
}
defer cleanup ()
if _ , err := client .Accept (ctx , chal ); err != nil {
continue
}
// A challenge is fulfilled and accepted: wait for the CA to validate.
if _ , err := client .WaitAuthorization (ctx , authz .URI ); err == nil {
return nil
}
}
if chal == nil {
return errors .New ("acme/autocert: no supported challenge type found" )
}
}
// create a token cert for the challenge response
var (
cert tls.Certificate
name string
)
// fulfill provisions a response to the challenge chal.
// The cleanup is non-nil only if provisioning succeeded.
func (m * Manager ) fulfill (ctx context.Context , client * acme.Client , chal * acme.Challenge ) (cleanup func (), err error ) {
switch chal .Type {
case "tls-sni-01" :
cert , name , err = client .TLSSNI01ChallengeCert (chal .Token )
cert , name , err := client .TLSSNI01ChallengeCert (chal .Token )
if err != nil {
return nil , err
}
m .putCertToken (ctx , name , & cert )
return func () { go m .deleteCertToken (name ) }, nil
case "tls-sni-02" :
cert , name , err = client .TLSSNI02ChallengeCert (chal .Token )
default :
err = fmt .Errorf ("acme/autocert: unknown challenge type %q" , chal .Type )
}
if err != nil {
return err
cert , name , err := client .TLSSNI02ChallengeCert (chal .Token )
if err != nil {
return nil , err
}
m .putCertToken (ctx , name , & cert )
return func () { go m .deleteCertToken (name ) }, nil
case "http-01" :
resp , err := client .HTTP01ChallengeResponse (chal .Token )
if err != nil {
return nil , err
}
p := client .HTTP01ChallengePath (chal .Token )
m .putHTTPToken (ctx , p , resp )
return func () { go m .deleteHTTPToken (p ) }, nil
}
m .putTokenCert (ctx , name , & cert )
defer func () {
// verification has ended at this point
// don't need token cert anymore
go m .deleteTokenCert (name )
}()
return nil , fmt .Errorf ("acme/autocert: unknown challenge type %q" , chal .Type )
}
// ready to fulfill the challenge
if _ , err := client .Accept (ctx , chal ); err != nil {
return err
func pickChallenge (typ string , chal []* acme.Challenge ) * acme.Challenge {
for _ , c := range chal {
if c .Type == typ {
return c
}
}
// wait for the CA to validate
_ , err = client .WaitAuthorization (ctx , authz .URI )
return err
return nil
}
// putTokenCert stores the cert under the named key in both m.tokenCert map
// putCertToken stores the cert under the named key in both m.certTokens map
// and m.Cache.
func (m * Manager ) putTokenCert (ctx context.Context , name string , cert * tls.Certificate ) {
m .tokenCertMu .Lock ()
defer m .tokenCertMu .Unlock ()
if m .tokenCert == nil {
m .tokenCert = make (map [string ]* tls.Certificate )
func (m * Manager ) putCertToken (ctx context.Context , name string , cert * tls.Certificate ) {
m .tokensMu .Lock ()
defer m .tokensMu .Unlock ()
if m .certTokens == nil {
m .certTokens = make (map [string ]* tls.Certificate )
}
m .tokenCert [name ] = cert
m .certTokens [name ] = cert
m .cachePut (ctx , name , cert )
}
// deleteTokenCert removes the token certificate for the specified domain name
// from both m.tokenCert map and m.Cache.
func (m * Manager ) deleteTokenCert (name string ) {
m .tokenCertMu .Lock ()
defer m .tokenCertMu .Unlock ()
delete (m .tokenCert , name )
// deleteCertToken removes the token certificate for the specified domain name
// from both m.certTokens map and m.Cache.
func (m * Manager ) deleteCertToken (name string ) {
m .tokensMu .Lock ()
defer m .tokensMu .Unlock ()
delete (m .certTokens , name )
if m .Cache != nil {
m .Cache .Delete (context .Background (), name )
}
}
// httpToken retrieves an existing http-01 token value from an in-memory map
// or the optional cache.
func (m * Manager ) httpToken (ctx context.Context , tokenPath string ) ([]byte , error ) {
m .tokensMu .RLock ()
defer m .tokensMu .RUnlock ()
if v , ok := m .httpTokens [tokenPath ]; ok {
return v , nil
}
if m .Cache == nil {
return nil , fmt .Errorf ("acme/autocert: no token at %q" , tokenPath )
}
return m .Cache .Get (ctx , httpTokenCacheKey (tokenPath ))
}
// putHTTPToken stores an http-01 token value using tokenPath as key
// in both in-memory map and the optional Cache.
//
// It ignores any error returned from Cache.Put.
func (m * Manager ) putHTTPToken (ctx context.Context , tokenPath , val string ) {
m .tokensMu .Lock ()
defer m .tokensMu .Unlock ()
if m .httpTokens == nil {
m .httpTokens = make (map [string ][]byte )
}
b := []byte (val )
m .httpTokens [tokenPath ] = b
if m .Cache != nil {
m .Cache .Put (ctx , httpTokenCacheKey (tokenPath ), b )
}
}
// deleteHTTPToken removes an http-01 token value from both in-memory map
// and the optional Cache, ignoring any error returned from the latter.
//
// If m.Cache is non-nil, it blocks until Cache.Delete returns without a timeout.
func (m * Manager ) deleteHTTPToken (tokenPath string ) {
m .tokensMu .Lock ()
defer m .tokensMu .Unlock ()
delete (m .httpTokens , tokenPath )
if m .Cache != nil {
m .Cache .Delete (context .Background (), httpTokenCacheKey (tokenPath ))
}
}
// httpTokenCacheKey returns a key at which an http-01 token value may be stored
// in the Manager's optional Cache.
func httpTokenCacheKey (tokenPath string ) string {
return "http-01-" + path .Base (tokenPath )
}
// renew starts a cert renewal timer loop, one per domain.
//
// The loop is scheduled in two cases:
OPTIONS?