From 486cd3ae1879e4f70d099b1d5a0984d00a931e63 Mon Sep 17 00:00:00 2001 From: James Munnelly Date: Fri, 9 Feb 2018 23:52:36 +0000 Subject: [PATCH] Copy across WIP acme v2 golang package --- third_party/crypto/acme/acme.go | 923 +++++++++++++++ third_party/crypto/acme/acme_test.go | 1123 +++++++++++++++++++ third_party/crypto/acme/integration_test.go | 146 +++ third_party/crypto/acme/jws.go | 158 +++ third_party/crypto/acme/jws_test.go | 320 ++++++ third_party/crypto/acme/types.go | 404 +++++++ third_party/crypto/acme/types_test.go | 63 ++ 7 files changed, 3137 insertions(+) create mode 100644 third_party/crypto/acme/acme.go create mode 100644 third_party/crypto/acme/acme_test.go create mode 100644 third_party/crypto/acme/integration_test.go create mode 100644 third_party/crypto/acme/jws.go create mode 100644 third_party/crypto/acme/jws_test.go create mode 100644 third_party/crypto/acme/types.go create mode 100644 third_party/crypto/acme/types_test.go diff --git a/third_party/crypto/acme/acme.go b/third_party/crypto/acme/acme.go new file mode 100644 index 000000000..a57c20e3f --- /dev/null +++ b/third_party/crypto/acme/acme.go @@ -0,0 +1,923 @@ +// 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 diff --git a/third_party/crypto/acme/acme_test.go b/third_party/crypto/acme/acme_test.go new file mode 100644 index 000000000..51fb63df0 --- /dev/null +++ b/third_party/crypto/acme/acme_test.go @@ -0,0 +1,1123 @@ +// 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 + +import ( + "context" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "math/big" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + "time" +) + +// Decodes a JWS-encoded request and unmarshals the decoded JSON into a provided +// interface. +func decodeJWSRequest(t *testing.T, v interface{}, r *http.Request) { + // Decode request + var req struct{ Payload string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatal(err) + } + payload, err := base64.RawURLEncoding.DecodeString(req.Payload) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(payload, v) + if err != nil { + t.Fatal(err) + } +} + +type jwsHead struct { + Alg string + Nonce string + JWK map[string]string `json:"jwk"` +} + +func decodeJWSHead(r *http.Request) (*jwsHead, error) { + var req struct{ Protected string } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + b, err := base64.RawURLEncoding.DecodeString(req.Protected) + if err != nil { + return nil, err + } + var head jwsHead + if err := json.Unmarshal(b, &head); err != nil { + return nil, err + } + return &head, nil +} + +func TestDiscover(t *testing.T) { + const ( + keyChange = "https://example.com/acme/key-change" + newAccount = "https://example.com/acme/new-account" + newNonce = "https://example.com/acme/new-nonce" + newOrder = "https://example.com/acme/new-order" + revokeCert = "https://example.com/acme/revoke-cert" + terms = "https://example.com/acme/terms" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "keyChange": %q, + "newAccount": %q, + "newNonce": %q, + "newOrder": %q, + "revokeCert": %q, + "meta": { + "termsOfService": %q + } + }`, keyChange, newAccount, newNonce, newOrder, revokeCert, terms) + })) + defer ts.Close() + c := Client{DirectoryURL: ts.URL} + dir, err := c.Discover(context.Background()) + if err != nil { + t.Fatal(err) + } + if dir.KeyChangeURL != keyChange { + t.Errorf("dir.KeyChangeURL = %q; want %q", dir.KeyChangeURL, keyChange) + } + if dir.NewAccountURL != newAccount { + t.Errorf("dir.NewAccountURL = %q; want %q", dir.NewAccountURL, newAccount) + } + if dir.NewNonceURL != newNonce { + t.Errorf("dir.NewNonceURL = %q; want %q", dir.NewNonceURL, newNonce) + } + if dir.RevokeCertURL != revokeCert { + t.Errorf("dir.RevokeCertURL = %q; want %q", dir.RevokeCertURL, revokeCert) + } + if dir.Terms != terms { + t.Errorf("dir.Terms = %q; want %q", dir.Terms, terms) + } +} + +func TestCreateAccount(t *testing.T) { + contacts := []string{"mailto:admin@example.com"} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "test-nonce") + return + } + if r.Method != "POST" { + t.Errorf("r.Method = %q; want POST", r.Method) + } + + var j struct { + Contact []string + TermsOfServiceAgreed bool + } + decodeJWSRequest(t, &j, r) + + if !reflect.DeepEqual(j.Contact, contacts) { + t.Errorf("j.Contact = %v; want %v", j.Contact, contacts) + } + if !j.TermsOfServiceAgreed { + t.Error("j.TermsOfServiceAgreed = false; want true") + } + + w.Header().Set("Location", "https://example.com/acme/account/1") + w.WriteHeader(http.StatusCreated) + b, _ := json.Marshal(contacts) + fmt.Fprintf(w, `{"status":"valid","orders":"https://example.com/acme/orders","contact":%s}`, b) + })) + defer ts.Close() + + c := Client{Key: testKeyEC, dir: &Directory{NewAccountURL: ts.URL, NewNonceURL: ts.URL}} + a := &Account{Contact: contacts, TermsAgreed: true} + var err error + if a, err = c.CreateAccount(context.Background(), a); err != nil { + t.Fatal(err) + } + if a.URL != "https://example.com/acme/account/1" { + t.Errorf("a.URL = %q; want https://example.com/acme/account/1", a.URL) + } + if a.OrdersURL != "https://example.com/acme/orders" { + t.Errorf("a.OrdersURL = %q; want https://example.com/acme/orders", a.OrdersURL) + } + if a.Status != StatusValid { + t.Errorf("a.Status = %q; want valid", a.Status) + } + if !reflect.DeepEqual(a.Contact, contacts) { + t.Errorf("a.Contact = %v; want %v", a.Contact, contacts) + } +} + +func TestUpdateAccount(t *testing.T) { + contacts := []string{"mailto:admin@example.com"} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "test-nonce") + return + } + if r.Method != "POST" { + t.Errorf("r.Method = %q; want POST", r.Method) + } + + var j struct { + Contact []string + } + decodeJWSRequest(t, &j, r) + + if !reflect.DeepEqual(j.Contact, contacts) { + t.Errorf("j.Contact = %v; want %v", j.Contact, contacts) + } + b, _ := json.Marshal(contacts) + fmt.Fprintf(w, `{"status":"valid","orders":"https://example.com/acme/orders","contact":%s}`, b) + })) + defer ts.Close() + + c := Client{Key: testKeyEC, dir: &Directory{NewNonceURL: ts.URL}} + a := &Account{URL: ts.URL, Contact: contacts} + var err error + if a, err = c.UpdateAccount(context.Background(), a); err != nil { + t.Fatal(err) + } + if a.OrdersURL != "https://example.com/acme/orders" { + t.Errorf("a.OrdersURL = %q; want https://example.com/acme/orders", a.OrdersURL) + } + if a.Status != StatusValid { + t.Errorf("a.Status = %q; want valid", a.Status) + } + if !reflect.DeepEqual(a.Contact, contacts) { + t.Errorf("a.Contact = %v; want %v", a.Contact, contacts) + } + if a.URL != ts.URL { + t.Errorf("a.URL = %q; want %q", a.URL, ts.URL) + } +} + +func TestGetAccount(t *testing.T) { + contacts := []string{"mailto:admin@example.com"} + + var ts *httptest.Server + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "test-nonce") + return + } + if r.Method != "POST" { + t.Errorf("r.Method = %q; want POST", r.Method) + } + + var req struct { + Existing bool `json:"onlyReturnExisting"` + } + decodeJWSRequest(t, &req, r) + if req.Existing { + w.Header().Set("Location", ts.URL) + w.WriteHeader(http.StatusOK) + return + } + b, _ := json.Marshal(contacts) + fmt.Fprintf(w, `{"status":"valid","orders":"https://example.com/acme/orders","contact":%s}`, b) + })) + defer ts.Close() + + c := Client{Key: testKeyEC, dir: &Directory{NewNonceURL: ts.URL, NewAccountURL: ts.URL}} + a, err := c.GetAccount(context.Background()) + if err != nil { + t.Fatal(err) + } + if a.OrdersURL != "https://example.com/acme/orders" { + t.Errorf("a.OrdersURL = %q; want https://example.com/acme/orders", a.OrdersURL) + } + if a.Status != StatusValid { + t.Errorf("a.Status = %q; want valid", a.Status) + } + if !reflect.DeepEqual(a.Contact, contacts) { + t.Errorf("a.Contact = %v; want %v", a.Contact, contacts) + } + if a.URL != ts.URL { + t.Errorf("a.URL = %q; want %q", a.URL, ts.URL) + } +} + +func TestCreateOrder(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "test-nonce") + return + } + if r.Method != "POST" { + t.Errorf("r.Method = %q; want POST", r.Method) + } + + var j struct { + Identifiers []struct { + Type string + Value string + } + } + decodeJWSRequest(t, &j, r) + + // Test request + if len(j.Identifiers) != 1 { + t.Errorf("len(j.Identifiers) = %d; want 1", len(j.Identifiers)) + } + if j.Identifiers[0].Type != "dns" { + t.Errorf("j.Identifier.Type = %q; want dns", j.Identifiers[0].Type) + } + if j.Identifiers[0].Value != "example.com" { + t.Errorf("j.Identifier.Value = %q; want example.com", j.Identifiers[0].Value) + } + + w.Header().Set("Location", "https://example.com/acme/order/1") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "identifiers": [{"type":"dns","value":"example.com"}], + "status":"pending", + "authorizations":["https://example.com/acme/order/1/1"], + "finalize":"https://example.com/acme/order/1/finalize" + }`) + })) + defer ts.Close() + + cl := Client{Key: testKeyEC, accountURL: "https://example.com/acme/account", dir: &Directory{NewOrderURL: ts.URL, NewNonceURL: ts.URL}} + o, err := cl.CreateOrder(context.Background(), NewOrder("example.com")) + if err != nil { + t.Fatal(err) + } + + if o.URL != "https://example.com/acme/order/1" { + t.Errorf("URL = %q; want https://example.com/acme/order/1", o.URL) + } + if o.Status != "pending" { + t.Errorf("Status = %q; want pending", o.Status) + } + if o.FinalizeURL != "https://example.com/acme/order/1/finalize" { + t.Errorf("FinalizeURL = %q; want https://example.com/acme/order/1/finalize", o.FinalizeURL) + } + + if n := len(o.Identifiers); n != 1 { + t.Fatalf("len(o.Identifiers) = %d; want 1", n) + } + if o.Identifiers[0].Type != "dns" { + t.Errorf("Identifiers[0].Type = %q; want dns", o.Identifiers[0].Type) + } + if o.Identifiers[0].Value != "example.com" { + t.Errorf("Identifiers[0].Value = %q; want example.com", o.Identifiers[0].Value) + } + + if n := len(o.Authorizations); n != 1 { + t.Fatalf("len(o.Authorizations) = %d; want 1", n) + } + if o.Authorizations[0] != "https://example.com/acme/order/1/1" { + t.Errorf("o.Authorizations[0] = %q; https://example.com/acme/order/1/1", o.Authorizations[0]) + } +} + +func TestGetAuthorization(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("r.Method = %q; want GET", r.Method) + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "identifier": {"type":"dns","value":"example.com"}, + "status":"pending", + "challenges":[ + { + "type":"http-01", + "status":"pending", + "url":"https://example.com/acme/challenge/publickey/id1", + "token":"token1" + }, + { + "type":"tls-sni-02", + "status":"pending", + "url":"https://example.com/acme/challenge/publickey/id2", + "token":"token2" + } + ] + }`) + })) + defer ts.Close() + + cl := Client{Key: testKeyEC, dir: &Directory{NewNonceURL: ts.URL}} + auth, err := cl.GetAuthorization(context.Background(), ts.URL) + if err != nil { + t.Fatal(err) + } + + if auth.Status != "pending" { + t.Errorf("Status = %q; want pending", auth.Status) + } + if auth.Identifier.Type != "dns" { + t.Errorf("Identifier.Type = %q; want dns", auth.Identifier.Type) + } + if auth.Identifier.Value != "example.com" { + t.Errorf("Identifier.Value = %q; want example.com", auth.Identifier.Value) + } + + if n := len(auth.Challenges); n != 2 { + t.Fatalf("len(set.Challenges) = %d; want 2", n) + } + + c := auth.Challenges[0] + if c.Type != "http-01" { + t.Errorf("c.Type = %q; want http-01", c.Type) + } + if c.URL != "https://example.com/acme/challenge/publickey/id1" { + t.Errorf("c.URI = %q; want https://example.com/acme/challenge/publickey/id1", c.URL) + } + if c.Token != "token1" { + t.Errorf("c.Token = %q; want token1", c.Token) + } + + c = auth.Challenges[1] + if c.Type != "tls-sni-02" { + t.Errorf("c.Type = %q; want tls-sni-02", c.Type) + } + if c.URL != "https://example.com/acme/challenge/publickey/id2" { + t.Errorf("c.URI = %q; want https://example.com/acme/challenge/publickey/id2", c.URL) + } + if c.Token != "token2" { + t.Errorf("c.Token = %q; want token2", c.Token) + } +} + +func TestWaitAuthorization(t *testing.T) { + var count int + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + w.Header().Set("Retry-After", "0") + if count > 1 { + fmt.Fprintf(w, `{"status":"valid"}`) + return + } + fmt.Fprintf(w, `{"status":"pending"}`) + })) + defer ts.Close() + + type res struct { + authz *Authorization + err error + } + done := make(chan res) + defer close(done) + go func() { + var client Client + a, err := client.WaitAuthorization(context.Background(), ts.URL) + done <- res{a, err} + }() + + select { + case <-time.After(5 * time.Second): + t.Fatal("WaitAuthz took too long to return") + case res := <-done: + if res.err != nil { + t.Fatalf("res.err = %v", res.err) + } + if res.authz == nil { + t.Fatal("res.authz is nil") + } + } +} + +func TestWaitAuthorizationInvalid(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{"status":"invalid"}`) + })) + defer ts.Close() + + res := make(chan error) + defer close(res) + go func() { + var client Client + _, err := client.WaitAuthorization(context.Background(), ts.URL) + res <- err + }() + + select { + case <-time.After(3 * time.Second): + t.Fatal("WaitAuthz took too long to return") + case err := <-res: + if err == nil { + t.Error("err is nil") + } + if _, ok := err.(AuthorizationError); !ok { + t.Errorf("err is %T; want *AuthorizationError", err) + } + } +} + +func TestWaitAuthorizationCancel(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Retry-After", "60") + fmt.Fprintf(w, `{"status":"pending"}`) + })) + defer ts.Close() + + res := make(chan error) + defer close(res) + go func() { + var client Client + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + _, err := client.WaitAuthorization(ctx, ts.URL) + res <- err + }() + + select { + case <-time.After(time.Second): + t.Fatal("WaitAuthz took too long to return") + case err := <-res: + if err == nil { + t.Error("err is nil") + } + } +} + +func TestDeactivateAuthorization(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "nonce") + return + } + switch r.URL.Path { + case "/1": + var req struct { + Status string + } + decodeJWSRequest(t, &req, r) + if req.Status != "deactivated" { + t.Errorf("req.Status = %q; want deactivated", req.Status) + } + case "/2": + w.WriteHeader(http.StatusInternalServerError) + case "/account": + w.Header().Set("Location", "https://example.com/acme/account/0") + w.Write([]byte("{}")) + } + })) + defer ts.Close() + client := &Client{Key: testKey, dir: &Directory{NewNonceURL: ts.URL, NewAccountURL: ts.URL + "/account"}} + ctx := context.Background() + if err := client.DeactivateAuthorization(ctx, ts.URL+"/1"); err != nil { + t.Errorf("err = %v", err) + } + if client.DeactivateAuthorization(ctx, ts.URL+"/2") == nil { + t.Error("nil error") + } +} + +func TestGetChallenge(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("r.Method = %q; want GET", r.Method) + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "type":"http-01", + "status":"pending", + "url":"https://example.com/acme/challenge/publickey/id1", + "validated": "2014-12-01T12:05:00Z", + "errors": [{ + "type": "urn:ietf:params:acme:error:malformed", + "detail": "rejected", + "subproblems": [ + { + "type": "urn:ietf:params:acme:error:unknown", + "detail": "invalid", + "identifier": { + "type": "dns", + "value": "_example.com" + } + } + ] + }], + "token":"token1"}`) + })) + defer ts.Close() + + cl := Client{Key: testKeyEC} + chall, err := cl.GetChallenge(context.Background(), ts.URL) + if err != nil { + t.Fatal(err) + } + + if chall.Status != "pending" { + t.Errorf("Status = %q; want pending", chall.Status) + } + if chall.Type != "http-01" { + t.Errorf("c.Type = %q; want http-01", chall.Type) + } + if chall.URL != "https://example.com/acme/challenge/publickey/id1" { + t.Errorf("c.URI = %q; want https://example.com/acme/challenge/publickey/id1", chall.URL) + } + if chall.Token != "token1" { + t.Errorf("c.Token = %q; want token1", chall.Token) + } + vt, _ := time.Parse(time.RFC3339, "2014-12-01T12:05:00Z") + if !chall.Validated.Equal(vt) { + t.Errorf("c.Validated = %v; want %v", chall.Validated, vt) + } + if l := len(chall.Errors); l != 1 { + t.Fatalf("len(c.Errors) = %d; want 1", l) + } + e := chall.Errors[0] + if e.Type != "urn:ietf:params:acme:error:malformed" { + t.Fatalf("e.Type = %q; want urn:ietf:params:acme:error:malformed", e.Type) + } + if e.Detail != "rejected" { + t.Fatalf("e.Detail = %q; want rejected", e.Detail) + } + if l := len(e.Subproblems); l != 1 { + t.Fatalf("len(e.Subproblems) = %d; want 1", l) + } + p := e.Subproblems[0] + if p.Type != "urn:ietf:params:acme:error:unknown" { + t.Fatalf("p.Type = %q; want urn:ietf:params:acme:error:unknown", p.Type) + } + if p.Detail != "invalid" { + t.Fatalf("p.Detail = %q; want rejected", p.Detail) + } + if p.Identifier.Type != "dns" { + t.Fatalf("p.Identifier.Type = %q; want dns", p.Identifier.Type) + } + if p.Identifier.Value != "_example.com" { + t.Fatalf("p.Identifier.Type = %q; want _example.com", p.Identifier.Value) + } +} + +func TestAcceptChallenge(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "test-nonce") + return + } + if r.Method != "POST" { + t.Errorf("r.Method = %q; want POST", r.Method) + } + + var j struct { + Auth string `json:"keyAuthorization"` + } + decodeJWSRequest(t, &j, r) + + keyAuth := "token1." + testKeyECThumbprint + if j.Auth != keyAuth { + t.Errorf(`keyAuthorization = %q; want %q`, j.Auth, keyAuth) + } + + // Respond to request + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "type":"http-01", + "status":"pending", + "url":"https://example.com/acme/challenge/publickey/id1", + "token":"token1", + "keyAuthorization":%q + }`, keyAuth) + })) + defer ts.Close() + + cl := Client{Key: testKeyEC, accountURL: "https://example.com/acme/account", dir: &Directory{NewNonceURL: ts.URL}} + c, err := cl.AcceptChallenge(context.Background(), &Challenge{ + URL: ts.URL, + Token: "token1", + }) + if err != nil { + t.Fatal(err) + } + + if c.Type != "http-01" { + t.Errorf("c.Type = %q; want http-01", c.Type) + } + if c.URL != "https://example.com/acme/challenge/publickey/id1" { + t.Errorf("c.URL = %q; want https://example.com/acme/challenge/publickey/id1", c.URL) + } + if c.Token != "token1" { + t.Errorf("c.Token = %q; want token1", c.Token) + } +} + +func TestFinalizeOrder(t *testing.T) { + notBefore := time.Now() + notAfter := notBefore.AddDate(0, 2, 0) + timeNow = func() time.Time { return notBefore } + var sampleCert []byte + + var ts *httptest.Server + var orderGets int + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "test-nonce") + return + } + if r.URL.Path == "/cert" && r.Method == "GET" { + pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: sampleCert}) + return + } + if r.URL.Path == "/order" { + status := "processing" + if orderGets > 0 { + status = "valid" + } + fmt.Fprintf(w, `{ + "identifiers": [{"type":"dns","value":"example.com"}], + "status":%q, + "authorizations":["https://example.com/acme/order/1/1"], + "finalize":"https://example.com/acme/order/1/finalize", + "certificate":%q + }`, status, ts.URL+"/cert") + orderGets++ + return + } + if r.Method != "POST" { + t.Errorf("r.Method = %q; want POST", r.Method) + } + + var j struct { + CSR string `json:"csr"` + } + decodeJWSRequest(t, &j, r) + + template := x509.Certificate{ + SerialNumber: big.NewInt(int64(1)), + Subject: pkix.Name{ + Organization: []string{"goacme"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + var err error + sampleCert, err = x509.CreateCertificate(rand.Reader, &template, &template, &testKeyEC.PublicKey, testKeyEC) + if err != nil { + t.Fatalf("Error creating certificate: %v", err) + } + + w.Header().Set("Location", "/order") + fmt.Fprintf(w, `{ + "identifiers": [{"type":"dns","value":"example.com"}], + "status":"processing", + "authorizations":["https://example.com/acme/order/1/1"], + "finalize":"https://example.com/acme/order/1/finalize" + }`) + })) + defer ts.Close() + + csr := x509.CertificateRequest{ + Version: 0, + Subject: pkix.Name{ + CommonName: "example.com", + Organization: []string{"goacme"}, + }, + } + csrb, err := x509.CreateCertificateRequest(rand.Reader, &csr, testKeyEC) + if err != nil { + t.Fatal(err) + } + + c := Client{Key: testKeyEC, accountURL: "https://example.com/acme/account", dir: &Directory{NewNonceURL: ts.URL}} + cert, err := c.FinalizeOrder(context.Background(), ts.URL, csrb) + if err != nil { + t.Fatal(err) + } + if cert == nil { + t.Errorf("cert is nil") + } +} + +func TestWaitOrderInvalid(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "nonce") + return + } + const order = `{"status":%q}` + if r.URL.Path == "/invalid" { + fmt.Fprintf(w, order, "invalid") + } + if r.URL.Path == "/pending" { + fmt.Fprintf(w, order, "pending") + } + })) + defer ts.Close() + + var client Client + _, err := client.WaitOrder(context.Background(), ts.URL+"/pending") + if e, ok := err.(OrderPendingError); ok { + if e.Order == nil { + t.Error("order is nil") + } + if e.Order.Status != "pending" { + t.Errorf("status = %q; want pending", e.Order.Status) + } + } else if err != nil { + t.Error(err) + } + + _, err = client.WaitOrder(context.Background(), ts.URL+"/invalid") + if e, ok := err.(OrderInvalidError); ok { + if e.Order == nil { + t.Error("order is nil") + } + if e.Order.Status != "invalid" { + t.Errorf("status = %q; want invalid", e.Order.Status) + } + } else if err != nil { + t.Error(err) + } +} + +func TestGetOrder(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{ + "identifiers": [{"type":"dns","value":"example.com"}], + "status":"valid", + "authorizations":["https://example.com/acme/order/1/1"], + "finalize":"https://example.com/acme/order/1/finalize", + "certificate":"https://example.com/acme/cert" + }`) + })) + defer ts.Close() + + var client Client + o, err := client.GetOrder(context.Background(), ts.URL) + if err != nil { + t.Fatal(err) + } + if o.URL != ts.URL { + t.Errorf("URL = %q; want %s", o.URL, ts.URL) + } + if o.Status != "valid" { + t.Errorf("Status = %q; want valid", o.Status) + } + if l := len(o.Authorizations); l != 1 { + t.Errorf("len(Authorizations) = %d; want 1", l) + } + if v := o.Authorizations[0]; v != "https://example.com/acme/order/1/1" { + t.Errorf("Authorizations[0] = %q; want https://example.com/acme/order/1/1", v) + } + if l := len(o.Identifiers); l != 1 { + t.Errorf("len(Identifiers) = %d; want 1", l) + } + if v := o.Identifiers[0].Type; v != "dns" { + t.Errorf("Identifiers[0].Type = %q; want dns", v) + } + if v := o.Identifiers[0].Value; v != "example.com" { + t.Errorf("Identifiers[0].Value = %q; want example.com", v) + } + if o.FinalizeURL != "https://example.com/acme/order/1/finalize" { + t.Errorf("FinalizeURL = %q; want https://example.com/acme/order/1/finalize", o.FinalizeURL) + } + if o.CertificateURL != "https://example.com/acme/cert" { + t.Errorf("FinalizeURL = %q; want https://example.com/acme/cert", o.CertificateURL) + } +} + +func TestRevokeCert(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Replay-Nonce", "nonce") + return + } + + var req struct { + Certificate string + Reason int + } + decodeJWSRequest(t, &req, r) + if req.Reason != 1 { + t.Errorf("req.Reason = %d; want 1", req.Reason) + } + // echo -n cert | base64 | tr -d '=' | tr '/+' '_-' + cert := "Y2VydA" + if req.Certificate != cert { + t.Errorf("req.Certificate = %q; want %q", req.Certificate, cert) + } + })) + defer ts.Close() + client := &Client{Key: testKeyEC, accountURL: "https://example.com/acme/account", dir: &Directory{RevokeCertURL: ts.URL, NewNonceURL: ts.URL}} + ctx := context.Background() + if err := client.RevokeCert(ctx, nil, []byte("cert"), CRLReasonKeyCompromise); err != nil { + t.Fatal(err) + } +} + +func TestNonce_add(t *testing.T) { + var c Client + c.addNonce(http.Header{"Replay-Nonce": {"nonce"}}) + c.addNonce(http.Header{"Replay-Nonce": {}}) + c.addNonce(http.Header{"Replay-Nonce": {"nonce"}}) + + nonces := map[string]struct{}{"nonce": {}} + if !reflect.DeepEqual(c.nonces, nonces) { + t.Errorf("c.nonces = %q; want %q", c.nonces, nonces) + } +} + +func TestNonce_addMax(t *testing.T) { + c := &Client{nonces: make(map[string]struct{})} + for i := 0; i < maxNonces; i++ { + c.nonces[fmt.Sprintf("%d", i)] = struct{}{} + } + c.addNonce(http.Header{"Replay-Nonce": {"nonce"}}) + if n := len(c.nonces); n != maxNonces { + t.Errorf("len(c.nonces) = %d; want %d", n, maxNonces) + } +} + +func TestNonce_fetch(t *testing.T) { + tests := []struct { + code int + nonce string + }{ + {http.StatusOK, "nonce1"}, + {http.StatusBadRequest, "nonce2"}, + {http.StatusOK, ""}, + } + var i int + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "HEAD" { + t.Errorf("%d: r.Method = %q; want HEAD", i, r.Method) + } + w.Header().Set("Replay-Nonce", tests[i].nonce) + w.WriteHeader(tests[i].code) + })) + defer ts.Close() + for ; i < len(tests); i++ { + test := tests[i] + c := &Client{dir: &Directory{NewNonceURL: ts.URL}} + n, err := c.fetchNonce(context.Background()) + if n != test.nonce { + t.Errorf("%d: n=%q; want %q", i, n, test.nonce) + } + switch { + case err == nil && test.nonce == "": + t.Errorf("%d: n=%q, err=%v; want non-nil error", i, n, err) + case err != nil && test.nonce != "": + t.Errorf("%d: n=%q, err=%v; want %q", i, n, err, test.nonce) + } + } +} + +func TestNonce_fetchError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTooManyRequests) + })) + defer ts.Close() + c := &Client{dir: &Directory{NewNonceURL: ts.URL}} + _, err := c.fetchNonce(context.Background()) + e, ok := err.(*Error) + if !ok { + t.Fatalf("err is %T; want *Error", err) + } + if e.StatusCode != http.StatusTooManyRequests { + t.Errorf("e.StatusCode = %d; want %d", e.StatusCode, http.StatusTooManyRequests) + } +} + +func TestNonce_postJWS(t *testing.T) { + var count int + seen := make(map[string]bool) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + w.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", count)) + if r.Method == "HEAD" { + // We expect the client do a HEAD request + // but only to fetch the first nonce. + return + } + // Make client.CreateOrder happy; we're not testing its result. + defer func() { + w.Header().Set("Location", "https://example.com/acme/order/1") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"status":"valid"}`)) + }() + + head, err := decodeJWSHead(r) + if err != nil { + t.Errorf("decodeJWSHead: %v", err) + return + } + if head.Nonce == "" { + t.Error("head.Nonce is empty") + return + } + if seen[head.Nonce] { + t.Errorf("nonce is already used: %q", head.Nonce) + } + seen[head.Nonce] = true + })) + defer ts.Close() + + client := Client{Key: testKey, accountURL: "https://example.com/acme/account", dir: &Directory{NewOrderURL: ts.URL, NewNonceURL: ts.URL}} + if _, err := client.CreateOrder(context.Background(), NewOrder("example.com")); err != nil { + t.Errorf("client.CreateOrder 1: %v", err) + } + // The second call should not generate another extra HEAD request. + if _, err := client.CreateOrder(context.Background(), NewOrder("example.com")); err != nil { + t.Errorf("client.CreateOrder 2: %v", err) + } + + if count != 3 { + t.Errorf("total requests count: %d; want 3", count) + } + if n := len(client.nonces); n != 1 { + t.Errorf("len(client.nonces) = %d; want 1", n) + } + for k := range seen { + if _, exist := client.nonces[k]; exist { + t.Errorf("used nonce %q in client.nonces", k) + } + } +} + +func TestRetryPostJWS(t *testing.T) { + var count int + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + w.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", count)) + if r.Method == "HEAD" { + // We expect the client to do 2 head requests to fetch + // nonces, one to start and another after getting badNonce + return + } + + head, err := decodeJWSHead(r) + if err != nil { + t.Errorf("decodeJWSHead: %v", err) + } else if head.Nonce == "" { + t.Error("head.Nonce is empty") + } else if head.Nonce == "nonce1" { + // return a badNonce error to force the call to retry + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`)) + return + } + // Make client.CreateOrder happy; we're not testing its result. + w.Header().Set("Location", "https://example.com/acme/order/1") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"status":"valid"}`)) + })) + defer ts.Close() + + client := Client{Key: testKey, accountURL: "https://example.com/acme/account", dir: &Directory{NewOrderURL: ts.URL, NewNonceURL: ts.URL}} + // This call will fail with badNonce, causing a retry + if _, err := client.CreateOrder(context.Background(), NewOrder("example.com")); err != nil { + t.Errorf("client.CreateOrder 1: %v", err) + } + if count != 4 { + t.Errorf("total requests count: %d; want 4", count) + } +} + +func TestErrorResponse(t *testing.T) { + s := `{ + "status": 400, + "type": "urn:acme:error:xxx", + "detail": "text" + }` + res := &http.Response{ + StatusCode: 400, + Status: "400 Bad Request", + Body: ioutil.NopCloser(strings.NewReader(s)), + Header: http.Header{"X-Foo": {"bar"}}, + } + err := responseError(res) + v, ok := err.(*Error) + if !ok { + t.Fatalf("err = %+v (%T); want *Error type", err, err) + } + if v.StatusCode != 400 { + t.Errorf("v.StatusCode = %v; want 400", v.StatusCode) + } + if v.Type != "urn:acme:error:xxx" { + t.Errorf("v.Type = %q; want urn:acme:error:xxx", v.Type) + } + if v.Detail != "text" { + t.Errorf("v.Detail = %q; want text", v.Detail) + } + if !reflect.DeepEqual(v.Header, res.Header) { + t.Errorf("v.Header = %+v; want %+v", v.Header, res.Header) + } +} + +func TestHTTP01Challenge(t *testing.T) { + const ( + token = "xxx" + // thumbprint is precomputed for testKeyEC in jws_test.go + value = token + "." + testKeyECThumbprint + urlpath = "/.well-known/acme-challenge/" + token + ) + client := &Client{Key: testKeyEC} + val, err := client.HTTP01ChallengeResponse(token) + if err != nil { + t.Fatal(err) + } + if val != value { + t.Errorf("val = %q; want %q", val, value) + } + if path := client.HTTP01ChallengePath(token); path != urlpath { + t.Errorf("path = %q; want %q", path, urlpath) + } +} + +func TestDNS01ChallengeRecord(t *testing.T) { + // echo -n xxx. | \ + // openssl dgst -binary -sha256 | \ + // base64 | tr -d '=' | tr '/+' '_-' + const value = "8DERMexQ5VcdJ_prpPiA0mVdp7imgbCgjsG4SqqNMIo" + + client := &Client{Key: testKeyEC} + val, err := client.DNS01ChallengeRecord("xxx") + if err != nil { + t.Fatal(err) + } + if val != value { + t.Errorf("val = %q; want %q", val, value) + } +} + +func TestBackoff(t *testing.T) { + tt := []struct{ min, max time.Duration }{ + {time.Second, 2 * time.Second}, + {2 * time.Second, 3 * time.Second}, + {4 * time.Second, 5 * time.Second}, + {8 * time.Second, 9 * time.Second}, + } + for i, test := range tt { + d := backoff(i, time.Minute) + if d < test.min || test.max < d { + t.Errorf("%d: d = %v; want between %v and %v", i, d, test.min, test.max) + } + } + + min, max := time.Second, 2*time.Second + if d := backoff(-1, time.Minute); d < min || max < d { + t.Errorf("d = %v; want between %v and %v", d, min, max) + } + + bound := 10 * time.Second + if d := backoff(100, bound); d != bound { + t.Errorf("d = %v; want %v", d, bound) + } +} diff --git a/third_party/crypto/acme/integration_test.go b/third_party/crypto/acme/integration_test.go new file mode 100644 index 000000000..c97998fc6 --- /dev/null +++ b/third_party/crypto/acme/integration_test.go @@ -0,0 +1,146 @@ +// Copyright 2018 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. + +// +build integration_test + +package acme_test + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "net" + "net/http" + "os" + "reflect" + "testing" + + "golang.org/x/crypto/acme" +) + +// This test works with Pebble and Let's Encrypt staging. +// For pebble use: ACME_DIRECTORY_URL=https://localhost:14000/dir go test +// For Let's Encrypt you'll need a publicly accessible HTTP server like `ngrok http 8080` and then +// TEST_HOST=xxx.ngrok.io:8080 ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory TEST_ACCOUNT_GET=1 TEST_REVOKE=1 go test +func TestIntegration(t *testing.T) { + dir := os.Getenv("ACME_DIRECTORY_URL") + testAccountGet := os.Getenv("TEST_ACCOUNT_GET") != "" + testRevoke := os.Getenv("TEST_REVOKE") != "" + testHost := os.Getenv("TEST_HOST") + if testHost == "" { + testHost = "localhost:5002" + } + testIdentifier, listenPort, _ := net.SplitHostPort(testHost) + if dir == "" { + t.Fatal("ACME_DIRECTORY_URL is required") + } + + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + c := &acme.Client{ + Key: key, + DirectoryURL: dir, + HTTPClient: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + }, + } + + a := &acme.Account{ + Contact: []string{"mailto:user@example.com"}, + TermsAgreed: true, + } + na, err := c.CreateAccount(context.Background(), a) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(a.Contact, na.Contact) { + t.Errorf("na.Contact = %q; want %q", na.Contact, a.Contact) + } + if na.URL == "" { + t.Fatal("empty na.URL") + } + + // this endpoint is not supported by pebble, so put it behind a flag + if testAccountGet { + na, err = c.GetAccount(context.Background()) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(a.Contact, na.Contact) { + t.Errorf("na.Contact = %q; want %q", na.Contact, a.Contact) + } + } + + order, err := c.CreateOrder(context.Background(), acme.NewOrder(testIdentifier)) + if err != nil { + t.Fatal(err) + } + auth, err := c.GetAuthorization(context.Background(), order.Authorizations[0]) + if err != nil { + t.Fatal(err) + } + + var challenge *acme.Challenge + for _, ch := range auth.Challenges { + if ch.Type == "http-01" { + challenge = ch + break + } + } + if challenge == nil { + t.Fatal("missing http-01 challenge") + } + + l, err := net.Listen("tcp", ":"+listenPort) + if err != nil { + t.Errorf("error listening for challenge: %s", err) + } + defer l.Close() + go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != c.HTTP01ChallengePath(challenge.Token) { + w.WriteHeader(404) + return + } + res, _ := c.HTTP01ChallengeResponse(challenge.Token) + w.Write([]byte(res)) + })) + + _, err = c.AcceptChallenge(context.Background(), challenge) + if err != nil { + t.Fatal(err) + } + + _, err = c.WaitAuthorization(context.Background(), order.Authorizations[0]) + if err != nil { + t.Fatal(err) + } + + certKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + csr, _ := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{testIdentifier}}, certKey) + der, err := c.FinalizeOrder(context.Background(), order.FinalizeURL, csr) + if err != nil { + t.Fatal(err) + } + + cert, err := x509.ParseCertificate(der[0]) + if err != nil { + t.Fatal(err) + } + if cert.DNSNames[0] != testIdentifier { + t.Errorf("unexpected DNSNames %v", cert.DNSNames) + } + + if testRevoke { + if err := c.RevokeCert(context.Background(), certKey, der[0], acme.CRLReasonUnspecified); err != nil { + t.Fatal(err) + } + } +} diff --git a/third_party/crypto/acme/jws.go b/third_party/crypto/acme/jws.go new file mode 100644 index 000000000..8ca8dbc91 --- /dev/null +++ b/third_party/crypto/acme/jws.go @@ -0,0 +1,158 @@ +// 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 + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + _ "crypto/sha512" // need for EC keys + "encoding/base64" + "encoding/json" + "fmt" + "math/big" +) + +// jwsEncodeJSON signs claimset using provided key and a nonce. +// The result is serialized in JSON format. +// See https://tools.ietf.org/html/rfc7515#section-7. +func jwsEncodeJSON(claimset interface{}, key crypto.Signer, accountURL, url, nonce string) ([]byte, error) { + alg, sha := jwsHasher(key) + if alg == "" || !sha.Available() { + return nil, ErrUnsupportedKey + } + var phead string + if accountURL == "" { + jwk, err := jwkEncode(key.Public()) + if err != nil { + return nil, err + } + phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, url) + } else { + phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, accountURL, nonce, url) + } + phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) + cs, err := json.Marshal(claimset) + if err != nil { + return nil, err + } + payload := base64.RawURLEncoding.EncodeToString(cs) + hash := sha.New() + hash.Write([]byte(phead + "." + payload)) + sig, err := jwsSign(key, sha, hash.Sum(nil)) + if err != nil { + return nil, err + } + + enc := struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` + }{ + Protected: phead, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(sig), + } + return json.Marshal(&enc) +} + +// jwkEncode encodes public part of an RSA or ECDSA key into a JWK. +// The result is also suitable for creating a JWK thumbprint. +// https://tools.ietf.org/html/rfc7517 +func jwkEncode(pub crypto.PublicKey) (string, error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + n := pub.N + e := big.NewInt(int64(pub.E)) + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, + base64.RawURLEncoding.EncodeToString(e.Bytes()), + base64.RawURLEncoding.EncodeToString(n.Bytes()), + ), nil + case *ecdsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.2.1 + p := pub.Curve.Params() + n := p.BitSize / 8 + if p.BitSize%8 != 0 { + n++ + } + x := pub.X.Bytes() + if n > len(x) { + x = append(make([]byte, n-len(x)), x...) + } + y := pub.Y.Bytes() + if n > len(y) { + y = append(make([]byte, n-len(y)), y...) + } + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, + p.Name, + base64.RawURLEncoding.EncodeToString(x), + base64.RawURLEncoding.EncodeToString(y), + ), nil + } + return "", ErrUnsupportedKey +} + +// jwsSign signs the digest using the given key. +// It returns ErrUnsupportedKey if the key type is unknown. +// The hash is used only for RSA keys. +func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) { + switch key := key.(type) { + case *rsa.PrivateKey: + return key.Sign(rand.Reader, digest, hash) + case *ecdsa.PrivateKey: + r, s, err := ecdsa.Sign(rand.Reader, key, digest) + if err != nil { + return nil, err + } + rb, sb := r.Bytes(), s.Bytes() + size := key.Params().BitSize / 8 + if size%8 > 0 { + size++ + } + sig := make([]byte, size*2) + copy(sig[size-len(rb):], rb) + copy(sig[size*2-len(sb):], sb) + return sig, nil + } + return nil, ErrUnsupportedKey +} + +// jwsHasher indicates suitable JWS algorithm name and a hash function +// to use for signing a digest with the provided key. +// It returns ("", 0) if the key is not supported. +func jwsHasher(key crypto.Signer) (string, crypto.Hash) { + switch key := key.(type) { + case *rsa.PrivateKey: + return "RS256", crypto.SHA256 + case *ecdsa.PrivateKey: + switch key.Params().Name { + case "P-256": + return "ES256", crypto.SHA256 + case "P-384": + return "ES384", crypto.SHA384 + case "P-521": + return "ES512", crypto.SHA512 + } + } + return "", 0 +} + +// JWKThumbprint creates a JWK thumbprint out of pub +// as specified in https://tools.ietf.org/html/rfc7638. +func JWKThumbprint(pub crypto.PublicKey) (string, error) { + jwk, err := jwkEncode(pub) + if err != nil { + return "", err + } + b := sha256.Sum256([]byte(jwk)) + return base64.RawURLEncoding.EncodeToString(b[:]), nil +} diff --git a/third_party/crypto/acme/jws_test.go b/third_party/crypto/acme/jws_test.go new file mode 100644 index 000000000..3d0b08482 --- /dev/null +++ b/third_party/crypto/acme/jws_test.go @@ -0,0 +1,320 @@ +// 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 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "testing" +) + +const ( + testKeyPEM = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq +WXfP9IeSKApbn34g8TuAS9g5zhq8ELQ3kmjr+KV86GAMgI6VAcGlq3QrzpTCf/30 +Ab7+zawrfRaFONa1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosq +EXeaIkVYBEhbhNu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZf +oyFyek380mHgJAumQ/I2fjj98/97mk3ihOY4AgVdCDj1z/GCoZkG5Rq7nbCGyosy +KWyDX00Zs+nNqVhoLeIvXC4nnWdJMZ6rogxyQQIDAQABAoIBACIEZTOI1Kao9nmV +9IeIsuaR1Y61b9neOF/MLmIVIZu+AAJFCMB4Iw11FV6sFodwpEyeZhx2WkpWVN+H +r19eGiLX3zsL0DOdqBJoSIHDWCCMxgnYJ6nvS0nRxX3qVrBp8R2g12Ub+gNPbmFm +ecf/eeERIVxfifd9VsyRu34eDEvcmKFuLYbElFcPh62xE3x12UZvV/sN7gXbawpP +G+w255vbE5MoaKdnnO83cTFlcHvhn24M/78qP7Te5OAeelr1R89kYxQLpuGe4fbS +zc6E3ym5Td6urDetGGrSY1Eu10/8sMusX+KNWkm+RsBRbkyKq72ks/qKpOxOa+c6 +9gm+Y8ECgYEA/iNUyg1ubRdH11p82l8KHtFC1DPE0V1gSZsX29TpM5jS4qv46K+s +8Ym1zmrORM8x+cynfPx1VQZQ34EYeCMIX212ryJ+zDATl4NE0I4muMvSiH9vx6Xc +7FmhNnaYzPsBL5Tm9nmtQuP09YEn8poiOJFiDs/4olnD5ogA5O4THGkCgYEA5MIL +qWYBUuqbEWLRtMruUtpASclrBqNNsJEsMGbeqBJmoMxdHeSZckbLOrqm7GlMyNRJ +Ne/5uWRGSzaMYuGmwsPpERzqEvYFnSrpjW5YtXZ+JtxFXNVfm9Z1gLLgvGpOUCIU +RbpoDckDe1vgUuk3y5+DjZihs+rqIJ45XzXTzBkCgYBWuf3segruJZy5rEKhTv+o +JqeUvRn0jNYYKFpLBeyTVBrbie6GkbUGNIWbrK05pC+c3K9nosvzuRUOQQL1tJbd +4gA3oiD9U4bMFNr+BRTHyZ7OQBcIXdz3t1qhuHVKtnngIAN1p25uPlbRFUNpshnt +jgeVoHlsBhApcs5DUc+pyQKBgDzeHPg/+g4z+nrPznjKnktRY1W+0El93kgi+J0Q +YiJacxBKEGTJ1MKBb8X6sDurcRDm22wMpGfd9I5Cv2v4GsUsF7HD/cx5xdih+G73 +c4clNj/k0Ff5Nm1izPUno4C+0IOl7br39IPmfpSuR6wH/h6iHQDqIeybjxyKvT1G +N0rRAoGBAKGD+4ZI/E1MoJ5CXB8cDDMHagbE3cq/DtmYzE2v1DFpQYu5I4PCm5c7 +EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO +9XWsXpbSTsRPj0sv1rB+UzBJ0PgjK4q2zOF0sNo7b1+6nlM3BWPx +-----END RSA PRIVATE KEY----- +` + + // This thumbprint is for the testKey defined above. + testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ" + + // openssl ecparam -name secp256k1 -genkey -noout + testKeyECPEM = ` +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIK07hGLr0RwyUdYJ8wbIiBS55CjnkMD23DWr+ccnypWLoAoGCCqGSM49 +AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5 +QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ== +-----END EC PRIVATE KEY----- +` + // openssl ecparam -name secp384r1 -genkey -noout + testKeyEC384PEM = ` +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAQ4lNtXRORWr1bgKR1CGysr9AJ9SyEk4jiVnlUWWUChmSNL+i9SLSD +Oe/naPqXJ6CgBwYFK4EEACKhZANiAAQzKtj+Ms0vHoTX5dzv3/L5YMXOWuI5UKRj +JigpahYCqXD2BA1j0E/2xt5vlPf+gm0PL+UHSQsCokGnIGuaHCsJAp3ry0gHQEke +WYXapUUFdvaK1R2/2hn5O+eiQM8YzCg= +-----END EC PRIVATE KEY----- +` + // openssl ecparam -name secp521r1 -genkey -noout + testKeyEC512PEM = ` +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIBSNZKFcWzXzB/aJClAb305ibalKgtDA7+70eEkdPt28/3LZMM935Z +KqYHh/COcxuu3Kt8azRAUz3gyr4zZKhlKUSgBwYFK4EEACOhgYkDgYYABAHUNKbx +7JwC7H6pa2sV0tERWhHhB3JmW+OP6SUgMWryvIKajlx73eS24dy4QPGrWO9/ABsD +FqcRSkNVTXnIv6+0mAF25knqIBIg5Q8M9BnOu9GGAchcwt3O7RDHmqewnJJDrbjd +GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ== +-----END EC PRIVATE KEY----- +` + // 1. openssl ec -in key.pem -noout -text + // 2. remove first byte, 04 (the header); the rest is X and Y + // 3. convert each with: echo | xxd -r -p | base64 -w 100 | tr -d '=' | tr '/+' '_-' + testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ" + testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk" + testKeyEC384PubX = "MyrY_jLNLx6E1-Xc79_y-WDFzlriOVCkYyYoKWoWAqlw9gQNY9BP9sbeb5T3_oJt" + testKeyEC384PubY = "Dy_lB0kLAqJBpyBrmhwrCQKd68tIB0BJHlmF2qVFBXb2itUdv9oZ-TvnokDPGMwo" + testKeyEC512PubX = "AdQ0pvHsnALsfqlraxXS0RFaEeEHcmZb44_pJSAxavK8gpqOXHvd5Lbh3LhA8atY738AGwMWpxFKQ1VNeci_r7SY" + testKeyEC512PubY = "AXbmSeogEiDlDwz0Gc670YYByFzC3c7tEMeap7CckkOtuN0Yaebqtv42dZH0MiikzSco2ROhagX-HOinG7gB78ax" + + // echo -n '{"crv":"P-256","kty":"EC","x":"","y":""}' | \ + // openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '/+' '_-' + testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU" +) + +var ( + testKey *rsa.PrivateKey + testKeyEC *ecdsa.PrivateKey + testKeyEC384 *ecdsa.PrivateKey + testKeyEC512 *ecdsa.PrivateKey +) + +func init() { + testKey = parseRSA(testKeyPEM, "testKeyPEM") + testKeyEC = parseEC(testKeyECPEM, "testKeyECPEM") + testKeyEC384 = parseEC(testKeyEC384PEM, "testKeyEC384PEM") + testKeyEC512 = parseEC(testKeyEC512PEM, "testKeyEC512PEM") +} + +func decodePEM(s, name string) []byte { + d, _ := pem.Decode([]byte(s)) + if d == nil { + panic("no block found in " + name) + } + return d.Bytes +} + +func parseRSA(s, name string) *rsa.PrivateKey { + b := decodePEM(s, name) + k, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + panic(fmt.Sprintf("%s: %v", name, err)) + } + return k +} + +func parseEC(s, name string) *ecdsa.PrivateKey { + b := decodePEM(s, name) + k, err := x509.ParseECPrivateKey(b) + if err != nil { + panic(fmt.Sprintf("%s: %v", name, err)) + } + return k +} + +func TestJWSEncodeJSON(t *testing.T) { + claims := struct{ Msg string }{"Hello JWS"} + // JWS signed with testKey and "nonce" as the nonce value + // JSON-serialized JWS fields are split for easier testing + const ( + // {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"url":"https://example.com","nonce":"nonce"} + protected = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" + + "IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" + + "SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" + + "QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" + + "VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" + + "NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" + + "QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" + + "bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" + + "ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" + + "b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" + + "UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6Imh0dHBzOi8vZXhh" + + "bXBsZS5jb20ifQ" + // {"Msg":"Hello JWS"} + payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ" + signature = "EtedUusG_N7NkuHRs9Ios6V0_VZdjYPut8vqRMDHvZQ3kZO0d5-9" + + "BivWINleGajAW29So64s4WYsITx2Y0g3obSw70Xsr8XVsVox2Wsx" + + "RJgd6KBNk1SGzqUW7-yEaS0fs0ax5SHPwpS9ek9WPCZ0MphfUH3d" + + "qK40x6dYbgY9mInfzf7L11QeRrQdJfGuef_74SJGTp6D4B5UrR2w" + + "m-AoSsRXY5A99U7J8YE9LFTg7pUQRSQWqqGZu-U9VDiB8bBvViVH" + + "1abbI5xHaSagDb1avfdIXqYv_QVeMXF67Nis8f963FOdX0zwjpob" + + "mpi-rsSmLBEtUkLERBIU_8JRdkXMcw" + ) + + b, err := jwsEncodeJSON(claims, testKey, "", "https://example.com", "nonce") + if err != nil { + t.Fatal(err) + } + var jws struct{ Protected, Payload, Signature string } + if err := json.Unmarshal(b, &jws); err != nil { + t.Fatal(err) + } + if jws.Protected != protected { + t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected) + } + if jws.Payload != payload { + t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload) + } + if jws.Signature != signature { + t.Errorf("signature:\n%s\nwant:\n%s", jws.Signature, signature) + } +} + +func TestJWSEncodeJSONEC(t *testing.T) { + tt := []struct { + key *ecdsa.PrivateKey + x, y string + alg, crv string + }{ + {testKeyEC, testKeyECPubX, testKeyECPubY, "ES256", "P-256"}, + {testKeyEC384, testKeyEC384PubX, testKeyEC384PubY, "ES384", "P-384"}, + {testKeyEC512, testKeyEC512PubX, testKeyEC512PubY, "ES512", "P-521"}, + } + for i, test := range tt { + claims := struct{ Msg string }{"Hello JWS"} + b, err := jwsEncodeJSON(claims, test.key, "", "https://example.com", "nonce") + if err != nil { + t.Errorf("%d: %v", i, err) + continue + } + var jws struct{ Protected, Payload, Signature string } + if err := json.Unmarshal(b, &jws); err != nil { + t.Errorf("%d: %v", i, err) + continue + } + + b, err = base64.RawURLEncoding.DecodeString(jws.Protected) + if err != nil { + t.Errorf("%d: jws.Protected: %v", i, err) + } + var head struct { + Alg string + Nonce string + JWK struct { + Crv string + Kty string + X string + Y string + } `json:"jwk"` + } + if err := json.Unmarshal(b, &head); err != nil { + t.Errorf("%d: jws.Protected: %v", i, err) + } + if head.Alg != test.alg { + t.Errorf("%d: head.Alg = %q; want %q", i, head.Alg, test.alg) + } + if head.Nonce != "nonce" { + t.Errorf("%d: head.Nonce = %q; want nonce", i, head.Nonce) + } + if head.JWK.Crv != test.crv { + t.Errorf("%d: head.JWK.Crv = %q; want %q", i, head.JWK.Crv, test.crv) + } + if head.JWK.Kty != "EC" { + t.Errorf("%d: head.JWK.Kty = %q; want EC", i, head.JWK.Kty) + } + if head.JWK.X != test.x { + t.Errorf("%d: head.JWK.X = %q; want %q", i, head.JWK.X, test.x) + } + if head.JWK.Y != test.y { + t.Errorf("%d: head.JWK.Y = %q; want %q", i, head.JWK.Y, test.y) + } + } +} + +func TestJWKThumbprintRSA(t *testing.T) { + // Key example from RFC 7638 + const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" + + "VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" + + "4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" + + "W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" + + "1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" + + "aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw" + const base64E = "AQAB" + const expected = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" + + b, err := base64.RawURLEncoding.DecodeString(base64N) + if err != nil { + t.Fatalf("Error parsing example key N: %v", err) + } + n := new(big.Int).SetBytes(b) + + b, err = base64.RawURLEncoding.DecodeString(base64E) + if err != nil { + t.Fatalf("Error parsing example key E: %v", err) + } + e := new(big.Int).SetBytes(b) + + pub := &rsa.PublicKey{N: n, E: int(e.Uint64())} + th, err := JWKThumbprint(pub) + if err != nil { + t.Error(err) + } + if th != expected { + t.Errorf("thumbprint = %q; want %q", th, expected) + } +} + +func TestJWKThumbprintEC(t *testing.T) { + // Key example from RFC 7520 + // expected was computed with + // echo -n '{"crv":"P-521","kty":"EC","x":"","y":""}' | \ + // openssl dgst -binary -sha256 | \ + // base64 | \ + // tr -d '=' | tr '/+' '_-' + const ( + base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" + + "KqjqvjyekWF-7ytDyRXYgCF5cj0Kt" + base64Y = "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUda" + + "QkAgDPrwQrJmbnX9cwlGfP-HqHZR1" + expected = "dHri3SADZkrush5HU_50AoRhcKFryN-PI6jPBtPL55M" + ) + + b, err := base64.RawURLEncoding.DecodeString(base64X) + if err != nil { + t.Fatalf("Error parsing example key X: %v", err) + } + x := new(big.Int).SetBytes(b) + + b, err = base64.RawURLEncoding.DecodeString(base64Y) + if err != nil { + t.Fatalf("Error parsing example key Y: %v", err) + } + y := new(big.Int).SetBytes(b) + + pub := &ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y} + th, err := JWKThumbprint(pub) + if err != nil { + t.Error(err) + } + if th != expected { + t.Errorf("thumbprint = %q; want %q", th, expected) + } +} + +func TestJWKThumbprintErrUnsupportedKey(t *testing.T) { + _, err := JWKThumbprint(struct{}{}) + if err != ErrUnsupportedKey { + t.Errorf("err = %q; want %q", err, ErrUnsupportedKey) + } +} diff --git a/third_party/crypto/acme/types.go b/third_party/crypto/acme/types.go new file mode 100644 index 000000000..2f153a1e3 --- /dev/null +++ b/third_party/crypto/acme/types.go @@ -0,0 +1,404 @@ +// Copyright 2016 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 + +import ( + "errors" + "fmt" + "net/http" + "time" +) + +// ACME server response statuses used to describe Account, Authorization, and Challenge states. +const ( + StatusUnknown = "unknown" + StatusPending = "pending" + StatusProcessing = "processing" + StatusValid = "valid" + StatusInvalid = "invalid" + StatusRevoked = "revoked" + StatusDeactivated = "deactivated" +) + +// CRLReasonCode identifies the reason for a certificate revocation. +type CRLReasonCode int + +// CRL reason codes as defined in RFC 5280. +const ( + CRLReasonUnspecified CRLReasonCode = 0 + CRLReasonKeyCompromise CRLReasonCode = 1 + CRLReasonCACompromise CRLReasonCode = 2 + CRLReasonAffiliationChanged CRLReasonCode = 3 + CRLReasonSuperseded CRLReasonCode = 4 + CRLReasonCessationOfOperation CRLReasonCode = 5 + CRLReasonCertificateHold CRLReasonCode = 6 + CRLReasonRemoveFromCRL CRLReasonCode = 8 + CRLReasonPrivilegeWithdrawn CRLReasonCode = 9 + CRLReasonAACompromise CRLReasonCode = 10 +) + +// ErrUnsupportedKey is returned when an unsupported key type is encountered. +var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported") + +// Error is an ACME error as defined in RFC 7807, Problem Details for HTTP APIs. +type Error struct { + // StatusCode is The HTTP status code generated by the origin server. + StatusCode int + + // Type is a URI that identifies the problem type, typically in + // a "urn:ietf:params:acme:error:xxx" form. + Type string + + // Detail is a human-readable explanation specific to this occurrence of the problem. + Detail string + + // Subproblems is an optional list of additional error information, usually + // indicating problems with specific identifiers during authorization. + Subproblems []Subproblem + + // Header is the original server error response headers. + // It may be nil. + Header http.Header +} + +func (e *Error) Error() string { + return fmt.Sprintf("acme: %s: %s", e.Type, e.Detail) +} + +// An Subproblem is additional error detail that is included in an Error, +// usually indicating a problem with a specific identifier during authorization. +type Subproblem struct { + // Type is a URI that identifies the subproblem type, typically in + // "urn:ietf:params:acme:error:xxx" form. + Type string + + // Detail is a human-readable explanation specific to this occurrence of the + // subproblem. + Detail string + + // Identifier optionally indicates the identifier that this subproblem is about. + Identifier *AuthzID +} + +// OrderInvalidError is returned when an order is marked as invalid. +type OrderInvalidError struct { + // Order is the order that is invalid. + Order *Order +} + +func (e OrderInvalidError) Error() string { + if e.Order == nil || e.Order.Error == nil { + return "acme: order is invalid" + } + return fmt.Sprintf("acme: invalid order (%s): %s", e.Order.Error.Type, e.Order.Error.Detail) +} + +// OrderPendingError is returned when an order is still pending after an +// attempted finalization. +type OrderPendingError struct { + // Order is the order that is pending. + Order *Order +} + +func (e OrderPendingError) Error() string { + return "acme: order is pending due to incomplete authorization" +} + +// AuthorizationError is returned when an authorization is marked as invalid. +type AuthorizationError struct { + // Authorization is the authorization that is invalid. + Authorization *Authorization +} + +func (e AuthorizationError) Error() string { + if e.Authorization == nil { + return "acme: authorization is invalid" + } + return fmt.Sprintf("acme: authorization for identifier %s is %s", e.Authorization.Identifier.Value, e.Authorization.Status) +} + +// RateLimit reports whether err represents a rate limit error and +// any Retry-After duration returned by the server. +// +// See the following for more details on rate limiting: +// https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-6.5 +func RateLimit(err error) (time.Time, bool) { + e, ok := err.(*Error) + if !ok || e.Type != "urn:ietf:params:acme:error:rateLimited" { + return time.Time{}, false + } + if e.Header == nil { + return time.Time{}, true + } + return retryAfter(e.Header.Get("Retry-After")), true +} + +// Account is a user account. It is associated with a private key. +type Account struct { + // URL uniquely identifies the account. + URL string + + // Status is the status of the account. Valid values are StatusValid, + // StatusDeactivated, and StatusRevoked. + Status string + + // Contact is a list of URLs that the server can use to contact the client + // for issues related to this account. + Contact []string + + // TermsAgreed indicates agreement with the terms of service. It is not + // modifiable after account creation. + TermsAgreed bool + + // OrdersURL is the URL used to fetch a list of orders submitted by this + // account. + OrdersURL string +} + +// Directory is ACME server discovery data. +type Directory struct { + // NewNonceURL is used to retrieve new nonces. + NewNonceURL string + + // NewAccountURL is used to create new accounts. + NewAccountURL string + + // NewOrderURL is used to create new orders. + NewOrderURL string + + // NewAuthzURL is used to create new authorizations. + NewAuthzURL string + + // RevokeCertURL is used to revoke a certificate. + RevokeCertURL string + + // KeyChangeURL is used to change the account key. + KeyChangeURL string + + // Terms is a URL identifying the current terms of service. + Terms string + + // Website is an HTTP or HTTPS URL locating a website + // providing more information about the ACME server. + Website string + + // CAA consists of lowercase hostname elements, which the ACME server + // recognises as referring to itself for the purposes of CAA record validation + // as defined in RFC6844. + CAA []string + + // ExternalAccountRequired, if true, indicates that the CA requires that all + // new account requests include an ExternalAccountBinding field associating + // the new account with an external account. + ExternalAccountRequired bool +} + +// NewOrder creates a new order with the domains provided, suitable for creating +// a TLS certificate order with CreateOrder. +func NewOrder(domains ...string) *Order { + o := &Order{Identifiers: make([]AuthzID, len(domains))} + for i, d := range domains { + o.Identifiers[i] = AuthzID{ + Type: "dns", + Value: d, + } + } + return o +} + +// An Order represents a request for a certificate and is used to track the +// progress through to issuance. +type Order struct { + // URL uniquely identifies the order. + URL string + + // Status is the status of the order. It will be one of StatusPending, + // StatusProcessing, StatusValid, and StatusInvalid. + Status string + + // Expires is the teimstamp after which the server will consider the order invalid. + Expires time.Time + + // Identifiers is a list of identifiers that the order pertains to. + Identifiers []AuthzID + + // NotBefore is an optional requested value of the notBefore field in the certificate. + NotBefore time.Time + + // NotAfter is an optional requested value of the notAfter field in the certificate. + NotAfter time.Time + + // Error is the error that occurred while processing the order, if any. + Error *Error + + // Authorizations is a list of URLs for authorizations that the client needs + // to complete before the requested certificate can be issued. For + // valid/invalid orders, these are the authorizations that were completed. + Authorizations []string + + // FinalizeURL is the URL that is used to finalize the Order. + FinalizeURL string + + // CertificateURL is the URL for the certificate that has been issued in + // response to this order. + CertificateURL string + + // RetryAfter is the timestamp, if any, to wait for before fetching this + // order again. + RetryAfter time.Time +} + +// A Challenge is a CA challenge for an identifier. +type Challenge struct { + // Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01". + Type string + + // URL is the URL where a challenge response can be posted. + URL string + + // Token is a random value that uniquely identifies the challenge. + Token string + + // Validated is the time at which the server validated this challenge. + Validated time.Time + + // Status identifies the status of this challenge. Valid values are + // StatusPending, StatusValid, and StatusInvalid. + Status string + + // Error indicates the errors that occurred while the server was validating + // this challenge. + Errors []*Error +} + +// Authorization encodes an authorization response. +type Authorization struct { + // URL uniquely identifies the authorization. + URL string + + // Status is the status of the authorization. Valid values are + // StatusPending, StatusProcessing, StatusValid, StatusInvalid, and + // StatusRevoked. + Status string + + // Identifier is the identifier that the account is authorized to represent. + Identifier AuthzID + + // Expires is the timestamp after which the server will consider this authorization invalid. + Expires *time.Time + + // Challenges is the list of challenges that the client can fulfill in order + // to prove posession of the identifier. For valid/invalid authorizations, + // this is the list of challenges that were used. + Challenges []*Challenge +} + +// AuthzID is an identifier that an account is authorized to represent. +type AuthzID struct { + Type string // The type of identifier, e.g. "dns". + Value string // The identifier itself, e.g. "example.org". +} + +type wireAuthzID struct { + Type string `json:"type"` + Value string `json:"value"` +} + +// wireAuthz is ACME JSON representation of Authorization objects. +type wireAuthz struct { + Status string + Challenges []wireChallenge + Expires *time.Time + Identifier struct { + Type string + Value string + } +} + +func (z *wireAuthz) authorization(url string) *Authorization { + a := &Authorization{ + URL: url, + Status: z.Status, + Expires: z.Expires, + Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value}, + Challenges: make([]*Challenge, len(z.Challenges)), + } + for i, v := range z.Challenges { + a.Challenges[i] = v.challenge() + } + return a +} + +// wireChallenge is ACME JSON challenge representation. +type wireChallenge struct { + URL string + Type string + Token string + Status string + Validated time.Time + Errors []*Error +} + +func (c *wireChallenge) challenge() *Challenge { + v := &Challenge{ + URL: c.URL, + Type: c.Type, + Token: c.Token, + Status: c.Status, + Validated: c.Validated, + Errors: c.Errors, + } + if v.Status == "" { + v.Status = StatusUnknown + } + return v +} + +// wireError is a subset of fields of the Problem Details object +// as described in https://tools.ietf.org/html/rfc7807#section-3.1. +type wireError struct { + Status int + Type string + Detail string + Subproblems []Subproblem +} + +func (e *wireError) error(h http.Header) *Error { + return &Error{ + StatusCode: e.Status, + Type: e.Type, + Detail: e.Detail, + Subproblems: e.Subproblems, + Header: h, + } +} + +type wireOrder struct { + Status string + Expires time.Time + Identifiers []AuthzID + NotBefore time.Time + NotAfter time.Time + Error *Error + Authorizations []string + Finalize string + Certificate string +} + +func (o *wireOrder) order(url string, retryHeader string) *Order { + return &Order{ + URL: url, + Status: o.Status, + Expires: o.Expires, + Identifiers: o.Identifiers, + NotBefore: o.NotBefore, + NotAfter: o.NotAfter, + Error: o.Error, + Authorizations: o.Authorizations, + FinalizeURL: o.Finalize, + CertificateURL: o.Certificate, + RetryAfter: retryAfter(retryHeader), + } +} diff --git a/third_party/crypto/acme/types_test.go b/third_party/crypto/acme/types_test.go new file mode 100644 index 000000000..c0564f4ca --- /dev/null +++ b/third_party/crypto/acme/types_test.go @@ -0,0 +1,63 @@ +// Copyright 2017 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 + +import ( + "errors" + "net/http" + "testing" + "time" +) + +func TestRateLimit(t *testing.T) { + now := time.Date(2017, 04, 27, 10, 0, 0, 0, time.UTC) + f := timeNow + defer func() { timeNow = f }() + timeNow = func() time.Time { return now } + + h120, hTime := http.Header{}, http.Header{} + h120.Set("Retry-After", "120") + hTime.Set("Retry-After", "Tue Apr 27 11:00:00 2017") + + err1 := &Error{ + Type: "urn:ietf:params:acme:error:nolimit", + Header: h120, + } + err2 := &Error{ + Type: "urn:ietf:params:acme:error:rateLimited", + Header: h120, + } + err3 := &Error{ + Type: "urn:ietf:params:acme:error:rateLimited", + Header: nil, + } + err4 := &Error{ + Type: "urn:ietf:params:acme:error:rateLimited", + Header: hTime, + } + + tt := []struct { + err error + res time.Time + ok bool + }{ + {}, + {err: errors.New("dummy")}, + {err: err1}, + {err: err2, res: now.Add(2 * time.Minute), ok: true}, + {err: err3, ok: true}, + {err: err4, res: now.Add(time.Hour), ok: true}, + } + for i, test := range tt { + res, ok := RateLimit(test.err) + if ok != test.ok { + t.Errorf("%d: RateLimit(%+v): ok = %v; want %v", i, test.err, ok, test.ok) + continue + } + if !res.Equal(test.res) { + t.Errorf("%d: RateLimit(%+v) = %v; want %v", i, test.err, res, test.res) + } + } +}