// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package acme provides an implementation of the // Automatic Certificate Management Environment (ACME) spec. // See https://tools.ietf.org/html/draft-ietf-acme-acme-09 for details. // // Most common scenarios will want to use autocert subdirectory instead, // which provides automatic access to certificates from Let's Encrypt // and any other ACME-based CA. // // This package is a work in progress and makes no API stability promises. package acme import ( "bytes" "context" "crypto" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "encoding/pem" "errors" "fmt" "io" "io/ioutil" "math/big" "net/http" "net/url" "strconv" "sync" "time" ) // LetsEncryptURL is the Directory endpoint of Let's Encrypt CA. const LetsEncryptURL = "https://acme-v02.api.letsencrypt.org/directory" const ( // max length of a certificate chain maxChainLen = 5 // max size of a certificate chain response, in bytes maxChainSize = (1 << 20) * maxChainLen // Max number of collected nonces kept in memory. // Expect usual peak of 1 or 2. maxNonces = 100 ) // Client is an ACME client. // The only required field is Key. An example of creating a client with a new key // is as follows: // // key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // if err != nil { // log.Fatal(err) // } // client := &Client{Key: key} // type Client struct { // Key is the account key used to register with a CA and sign requests. // Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey. Key crypto.Signer // HTTPClient optionally specifies an HTTP client to use // instead of http.DefaultClient. HTTPClient *http.Client // DirectoryURL points to the CA directory endpoint. // If empty, LetsEncryptURL is used. // Mutating this value after a successful call of Client's Discover method // will have no effect. DirectoryURL string noncesMu sync.Mutex nonces map[string]struct{} // nonces collected from previous responses urlMu sync.Mutex // urlMu guards writes to dir, accountURL, ordersURL dir *Directory // cached result of Client's Discover method accountURL string ordersURL string } // Discover performs ACME server discovery using c.DirectoryURL. // // It caches successful result. So, subsequent calls will not result in // a network round-trip. This also means mutating c.DirectoryURL after successful call // of this method will have no effect. func (c *Client) Discover(ctx context.Context) (Directory, error) { c.urlMu.Lock() defer c.urlMu.Unlock() if c.dir != nil { return *c.dir, nil } dirURL := c.DirectoryURL if dirURL == "" { dirURL = LetsEncryptURL } res, err := c.get(ctx, dirURL) if err != nil { return Directory{}, err } defer res.Body.Close() c.addNonce(res.Header) if res.StatusCode != http.StatusOK { return Directory{}, responseError(res) } var v struct { NewNonce string NewAccount string NewOrder string NewAuthz string RevokeCert string KeyChange string Meta struct { TermsOfService string Website string CAAIdentities []string ExternalAccountRequired bool } } if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return Directory{}, err } c.dir = &Directory{ NewNonceURL: v.NewNonce, NewAccountURL: v.NewAccount, NewOrderURL: v.NewOrder, NewAuthzURL: v.NewAuthz, RevokeCertURL: v.RevokeCert, KeyChangeURL: v.KeyChange, Terms: v.Meta.TermsOfService, Website: v.Meta.Website, CAA: v.Meta.CAAIdentities, ExternalAccountRequired: v.Meta.ExternalAccountRequired, } return *c.dir, nil } // CreateOrder creates a new certificate order. The input order argument is not // modified and can be built using NewOrderWithDomains. func (c *Client) CreateOrder(ctx context.Context, order *Order) (*Order, error) { if _, err := c.Discover(ctx); err != nil { return nil, err } req := struct { Identifiers []wireAuthzID `json:"identifiers"` NotBefore string `json:"notBefore,omitempty"` NotAfter string `json:"notAfter,omitempty"` }{ Identifiers: make([]wireAuthzID, len(order.Identifiers)), } for i, id := range order.Identifiers { req.Identifiers[i] = wireAuthzID{ Type: id.Type, Value: id.Value, } } if !order.NotBefore.IsZero() { req.NotBefore = order.NotBefore.Format(time.RFC3339) } if !order.NotAfter.IsZero() { req.NotAfter = order.NotAfter.Format(time.RFC3339) } res, err := c.postWithJWSAccount(ctx, c.dir.NewOrderURL, req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { return nil, responseError(res) } var v wireOrder if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, err } l, err := resolveLocation(c.dir.NewOrderURL, res.Header) if err != nil { return nil, err } o := v.order(l, "") if o.Status == StatusInvalid { return nil, OrderInvalidError{o} } return o, nil } // FinalizeOrder finalizes an order using the Certificate Signing Request csr // encoded in DER format. If the order has not been fully authorized, // an OrderPendingError will be returned. // // After requesting finalization, FinalizOrder polls the order using WaitOrder // until it is finalized and then fetches the associated certificate and returns // it. // // Callers are encouraged to parse the returned certificate chain to ensure it // is valid and has the expected attributes. func (c *Client) FinalizeOrder(ctx context.Context, finalizeURL string, csr []byte) (der [][]byte, err error) { req := struct { CSR string `json:"csr"` }{ CSR: base64.RawURLEncoding.EncodeToString(csr), } res, err := c.postWithJWSAccount(ctx, finalizeURL, req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, responseError(res) } var v wireOrder if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, err } l, err := resolveLocation(finalizeURL, res.Header) if err != nil { return nil, err } o := v.order(l, res.Header.Get("Retry-After")) if o.Status == StatusProcessing || o.Status == StatusPending { o, err = c.WaitOrder(ctx, o.URL) if err != nil { return nil, err } } if o.Status != StatusValid { return nil, fmt.Errorf("acme: unexpected order status %q", o.Status) } return c.getCert(ctx, o.CertificateURL) } // GetOrder retrieves an order identified by url. // // If a caller needs to poll an order until its status is final, // see the WaitOrder method. func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) { res, err := c.get(ctx, url) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { err = responseError(res) return nil, err } var v wireOrder if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, err } return v.order(url, res.Header.Get("Retry-After")), nil } // WaitOrder waits for an order to transition from StatusProcessing to a final // state (StatusValid/StatusInvalid), it retries the request until the order is // final, ctx is cancelled by the caller, or an error response is received. // // It returns a non-nil Order only if its Status is StatusValid. In all other // cases WaitOrder returns an error. If the Status is StatusInvalid, the // returned error will be of type OrderInvalidError. If the status is // StatusPending, the returned error will be of type OrderPendingError. func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) { sleep := timeSleeper(ctx) for { o, err := c.GetOrder(ctx, url) if err != nil { return nil, err } switch o.Status { case StatusValid: return o, nil case StatusInvalid: return nil, OrderInvalidError{o} case StatusPending: return nil, OrderPendingError{o} case StatusProcessing: // continue retry loop default: return nil, fmt.Errorf("acme: unexpected order status %q", o.Status) } if err := sleep(o.RetryAfter); err != nil { return nil, err } } } // RevokeCert revokes a previously issued certificate cert, provided in DER // format. // // If key is nil, the account must have been used to issue the certificate or // have valid authorizations for all of the identifiers in the certificate. If // key is provided, it must be the certificate's private key. func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error { if _, err := c.Discover(ctx); err != nil { return err } body := &struct { Cert string `json:"certificate"` Reason int `json:"reason"` }{ Cert: base64.RawURLEncoding.EncodeToString(cert), Reason: int(reason), } var res *http.Response var err error if key == nil { res, err = c.postWithJWSAccount(ctx, c.dir.RevokeCertURL, body) } else { res, err = c.postWithJWSKey(ctx, key, c.dir.RevokeCertURL, body) } if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return responseError(res) } return nil } // CreateAccount creates a new account. It returns the account details from the // server and does not modify the account argument that it is called with. func (c *Client) CreateAccount(ctx context.Context, a *Account) (*Account, error) { if _, err := c.Discover(ctx); err != nil { return nil, err } return c.doAccount(ctx, c.dir.NewAccountURL, false, a) } // GetAccount retrieves the account that the client is configured with. func (c *Client) GetAccount(ctx context.Context) (*Account, error) { if _, err := c.Discover(ctx); err != nil { return nil, err } return c.doAccount(ctx, c.dir.NewAccountURL, true, nil) } // UpdateAccount updates an existing account. It returns an updated account // copy. The provided account is not modified. func (c *Client) UpdateAccount(ctx context.Context, a *Account) (*Account, error) { return c.doAccount(ctx, a.URL, false, a) } // GetAuthorization retrieves an authorization identified by the given URL. // // If a caller needs to poll an authorization until its status is final, // see the WaitAuthorization method. func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, error) { res, err := c.get(ctx, url) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, responseError(res) } var v wireAuthz if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: invalid response: %v", err) } return v.authorization(url), nil } // DeactivateAuthorization relinquishes an existing authorization identified by // the given URL. // // If successful, the caller will be required to obtain a new authorization // before a new certificate for the domain associated with the authorization is // issued. // // It does not revoke existing certificates. func (c *Client) DeactivateAuthorization(ctx context.Context, url string) error { res, err := c.postWithJWSAccount(ctx, url, json.RawMessage(`{"status":"deactivated"}`)) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return responseError(res) } return nil } // WaitAuthorization retrieves authorization details. If the authorization is not in // a final state (StatusValid/StatusInvalid), it retries the request until the authorization // is final, ctx is cancelled by the caller, or an error response is received. // // It returns a non-nil Authorization only if its Status is StatusValid. // In all other cases WaitAuthorization returns an error. // If the Status is StatusInvalid or StatusDeactivated, the returned error will be of type AuthorizationError. func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) { sleep := sleeper(ctx) for { res, err := c.get(ctx, url) if err != nil { return nil, err } if res.StatusCode != http.StatusOK { err = responseError(res) res.Body.Close() return nil, err } var raw wireAuthz err = json.NewDecoder(res.Body).Decode(&raw) res.Body.Close() if err != nil { return nil, err } switch raw.Status { case StatusValid: return raw.authorization(url), nil case StatusInvalid, StatusDeactivated: return nil, AuthorizationError{raw.authorization(url)} case StatusPending, StatusProcessing: // fall through to sleep default: return nil, fmt.Errorf("acme: unknown authorization status %q", raw.Status) } if err := sleep(res.Header.Get("Retry-After")); err != nil { return nil, err } } } // GetChallenge retrieves the current status of a challenge. // // A client typically polls a challenge status using this method. func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) { res, err := c.get(ctx, url) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, responseError(res) } v := wireChallenge{URL: url} if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: invalid response: %v", err) } return v.challenge(), nil } // AcceptChallenge informs the server that the client accepts one of its // authorization challenges previously obtained with // CreateOrder/GetAuthorization. // // The server will then perform the validation asynchronously. func (c *Client) AcceptChallenge(ctx context.Context, chal *Challenge) (*Challenge, error) { auth, err := keyAuth(c.Key.Public(), chal.Token) if err != nil { return nil, err } req := struct { Auth string `json:"keyAuthorization"` }{auth} res, err := c.postWithJWSAccount(ctx, chal.URL, req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, responseError(res) } var v wireChallenge if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: invalid response: %v", err) } return v.challenge(), nil } // DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response. // A TXT record containing the returned value must be provisioned under // "_acme-challenge" name of the domain being validated. // // The token argument is a Challenge.Token value. func (c *Client) DNS01ChallengeRecord(token string) (string, error) { ka, err := keyAuth(c.Key.Public(), token) if err != nil { return "", err } b := sha256.Sum256([]byte(ka)) return base64.RawURLEncoding.EncodeToString(b[:]), nil } // HTTP01ChallengeResponse returns the response for an http-01 challenge. // Servers should respond with the value to HTTP requests at the URL path // provided by HTTP01ChallengePath to validate the challenge and prove control // over a domain name. // // The token argument is a Challenge.Token value. func (c *Client) HTTP01ChallengeResponse(token string) (string, error) { return keyAuth(c.Key.Public(), token) } // HTTP01ChallengePath returns the URL path at which the response for an http-01 challenge // should be provided by the servers. // The response value can be obtained with HTTP01ChallengeResponse. // // The token argument is a Challenge.Token value. func (c *Client) HTTP01ChallengePath(token string) string { return "/.well-known/acme-challenge/" + token } // doAccount creates, updates, and reads accounts. // // A non-nil acct argument indicates whether the intention is to mutate data of // the Account. Only the Contact field can be updated. func (c *Client) doAccount(ctx context.Context, url string, getExistingWithKey bool, acct *Account) (*Account, error) { req := struct { Contact []string `json:"contact,omitempty"` TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"` GetExisting bool `json:"onlyReturnExisting,omitempty"` }{ GetExisting: getExistingWithKey, } var accountURL string if url != c.dir.NewAccountURL { accountURL = url } if acct != nil { req.Contact = acct.Contact req.TermsAgreed = acct.TermsAgreed } res, err := c.retryPostJWS(ctx, c.Key, accountURL, url, req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode < 200 || res.StatusCode > 299 { return nil, responseError(res) } if getExistingWithKey { l, err := resolveLocation(url, res.Header) if err != nil { return nil, err } return c.doAccount(ctx, l, false, nil) } var v struct { Status string Contact []string Orders string } if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: invalid response: %v", err) } l, err := resolveLocation(url, res.Header) if err != nil { return nil, err } a := &Account{ URL: l, Status: v.Status, Contact: v.Contact, OrdersURL: v.Orders, } if a.URL == "" { a.URL = url } c.urlMu.Lock() defer c.urlMu.Unlock() c.accountURL = a.URL c.ordersURL = a.OrdersURL return a, nil } // cacheAccount ensures that the account URL is cached and returns it. func (c *Client) cacheAccountURL(ctx context.Context) (string, error) { c.urlMu.Lock() defer c.urlMu.Unlock() if c.accountURL != "" { return c.accountURL, nil } res, err := c.postWithJWSKey(ctx, c.Key, c.dir.NewAccountURL, json.RawMessage(`{"onlyReturnExisting":true}`)) if err != nil { return "", err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return "", responseError(res) } var v struct { Orders string } if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return "", err } l, err := resolveLocation(c.dir.NewAccountURL, res.Header) if err != nil { return "", err } c.accountURL = l c.ordersURL = v.Orders return c.accountURL, nil } func (c *Client) postWithJWSKey(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, error) { return c.retryPostJWS(ctx, key, "", url, body) } func (c *Client) postWithJWSAccount(ctx context.Context, url string, body interface{}) (*http.Response, error) { accountURL, err := c.cacheAccountURL(ctx) if err != nil { return nil, err } return c.retryPostJWS(ctx, c.Key, accountURL, url, body) } // retryPostJWS will retry calls to postJWS if there is a badNonce error, // clearing the stored nonces after each error. // If the response was 4XX-5XX, then responseError is called on the body, // the body is closed, and the error returned. func (c *Client) retryPostJWS(ctx context.Context, key crypto.Signer, accountURL, url string, body interface{}) (*http.Response, error) { sleep := sleeper(ctx) for { res, err := c.postJWS(ctx, key, accountURL, url, body) if err != nil { return nil, err } // handle errors 4XX-5XX with responseError if res.StatusCode >= 400 && res.StatusCode <= 599 { err := responseError(res) res.Body.Close() if ae, ok := err.(*Error); ok && ae.Type == "urn:ietf:params:acme:error:badNonce" { // clear any nonces that we might've stored that might now be // considered bad c.clearNonces() retry := res.Header.Get("Retry-After") if err := sleep(retry); err != nil { return nil, err } continue } return nil, err } return res, nil } } // postJWS signs the body with the given key and POSTs it to the provided url. // The body argument must be JSON-serializable. // The accountURL should be empty for account creation and certificate revocation. func (c *Client) postJWS(ctx context.Context, key crypto.Signer, accountURL, url string, body interface{}) (*http.Response, error) { nonce, err := c.popNonce(ctx) if err != nil { return nil, err } b, err := jwsEncodeJSON(body, key, accountURL, url, nonce) if err != nil { return nil, err } res, err := c.post(ctx, url, "application/jose+json", bytes.NewReader(b)) if err != nil { return nil, err } c.addNonce(res.Header) return res, nil } // popNonce returns a nonce value previously stored with c.addNonce // or fetches a fresh one. func (c *Client) popNonce(ctx context.Context) (string, error) { c.noncesMu.Lock() defer c.noncesMu.Unlock() if len(c.nonces) == 0 { return c.fetchNonce(ctx) } var nonce string for nonce = range c.nonces { delete(c.nonces, nonce) break } return nonce, nil } // clearNonces clears any stored nonces func (c *Client) clearNonces() { c.noncesMu.Lock() defer c.noncesMu.Unlock() c.nonces = make(map[string]struct{}) } // addNonce stores a nonce value found in h (if any) for future use. func (c *Client) addNonce(h http.Header) { v := nonceFromHeader(h) if v == "" { return } c.noncesMu.Lock() defer c.noncesMu.Unlock() if len(c.nonces) >= maxNonces { return } if c.nonces == nil { c.nonces = make(map[string]struct{}) } c.nonces[v] = struct{}{} } func (c *Client) httpClient() *http.Client { if c.HTTPClient != nil { return c.HTTPClient } return http.DefaultClient } func (c *Client) get(ctx context.Context, urlStr string) (*http.Response, error) { req, err := http.NewRequest("GET", urlStr, nil) if err != nil { return nil, err } return c.do(ctx, req) } func (c *Client) head(ctx context.Context, urlStr string) (*http.Response, error) { req, err := http.NewRequest("HEAD", urlStr, nil) if err != nil { return nil, err } return c.do(ctx, req) } func (c *Client) post(ctx context.Context, urlStr, contentType string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest("POST", urlStr, body) if err != nil { return nil, err } req.Header.Set("Content-Type", contentType) return c.do(ctx, req) } func (c *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) { res, err := c.httpClient().Do(req.WithContext(ctx)) if err != nil { select { case <-ctx.Done(): // Prefer the unadorned context error. // (The acme package had tests assuming this, previously from ctxhttp's // behavior, predating net/http supporting contexts natively) // TODO(bradfitz): reconsider this in the future. But for now this // requires no test updates. return nil, ctx.Err() default: return nil, err } } return res, nil } func (c *Client) fetchNonce(ctx context.Context) (string, error) { resp, err := c.head(ctx, c.dir.NewNonceURL) if err != nil { return "", err } defer resp.Body.Close() nonce := nonceFromHeader(resp.Header) if nonce == "" { if resp.StatusCode > 299 { return "", responseError(resp) } return "", errors.New("acme: nonce not found") } return nonce, nil } func nonceFromHeader(h http.Header) string { return h.Get("Replay-Nonce") } func (c *Client) getCert(ctx context.Context, url string) ([][]byte, error) { res, err := c.get(ctx, url) if err != nil { return nil, err } defer res.Body.Close() data, err := ioutil.ReadAll(io.LimitReader(res.Body, maxChainSize+1)) if err != nil { return nil, fmt.Errorf("acme: error getting certificate: %v", err) } if len(data) > maxChainSize { return nil, errors.New("acme: certificate chain is too big") } var chain [][]byte for { var p *pem.Block p, data = pem.Decode(data) if p == nil { if len(chain) == 0 { return nil, errors.New("acme: invalid PEM certificate chain") } break } if len(chain) == maxChainLen { return nil, errors.New("acme: certificate chain is too long") } if p.Type != "CERTIFICATE" { return nil, fmt.Errorf("acme: invalid PEM block type %q", p.Type) } chain = append(chain, p.Bytes) } return chain, nil } // responseError creates an error of Error type from resp. func responseError(resp *http.Response) error { // don't care if ReadAll returns an error: // json.Unmarshal will fail in that case anyway b, _ := ioutil.ReadAll(resp.Body) e := &wireError{Status: resp.StatusCode} if err := json.Unmarshal(b, e); err != nil { // this is not a regular error response: // populate detail with anything we received, // e.Status will already contain HTTP response code value e.Detail = string(b) if e.Detail == "" { e.Detail = resp.Status } } return e.error(resp.Header) } // sleeper returns a function that accepts the Retry-After HTTP header value // and an increment that's used with backoff to increasingly sleep on // consecutive calls until the context is done. If the Retry-After header // cannot be parsed, then backoff is used with a maximum sleep time of 10 // seconds. func sleeper(ctx context.Context) func(ra string) error { sleep := timeSleeper(ctx) return func(ra string) error { return sleep(retryAfter(ra)) } } func timeSleeper(ctx context.Context) func(time.Time) error { var count int return func(t time.Time) error { d := backoff(count, 10*time.Second) count++ if !t.IsZero() { d = t.Sub(timeNow()) } wakeup := time.NewTimer(d) defer wakeup.Stop() select { case <-ctx.Done(): return ctx.Err() case <-wakeup.C: return nil } } } // retryAfter parses a Retry-After HTTP header value, // trying to convert v into an int (seconds) or use http.ParseTime otherwise. func retryAfter(v string) time.Time { if i, err := strconv.Atoi(v); err == nil { return timeNow().Add(time.Duration(i) * time.Second) } t, err := http.ParseTime(v) if err != nil { return time.Time{} } return t } // backoff computes a duration after which an n+1 retry iteration should occur // using truncated exponential backoff algorithm. // // The n argument is always bounded between 0 and 30. // The max argument defines upper bound for the returned value. func backoff(n int, max time.Duration) time.Duration { if n < 0 { n = 0 } if n > 30 { n = 30 } var d time.Duration if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil { d = time.Duration(x.Int64()) * time.Millisecond } d += time.Duration(1< max { return max } return d } // keyAuth generates a key authorization string for a given token. func keyAuth(pub crypto.PublicKey, token string) (string, error) { th, err := JWKThumbprint(pub) if err != nil { return "", err } return fmt.Sprintf("%s.%s", token, th), nil } func resolveLocation(base string, h http.Header) (string, error) { u, err := url.Parse(base) if err != nil { return "", err } u, err = u.Parse(h.Get("Location")) if err != nil { return "", fmt.Errorf("acme: error parsing Location: %s", err) } return u.String(), nil } // timeNow is useful for testing for fixed current time. var timeNow = time.Now