From 5bd4f74ad5a5488ee8e1c935fd0feca773f2e696 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 15 Aug 2021 22:16:55 -0700 Subject: [PATCH] acme: remove support for pre-RFC 8555 ACME spec LetsEncrypt removed it anyway. No API changes. Just a lot of deleted code. Fixes golang/go#46654 Change-Id: I6e22f95bbc771abde8a445d7a7cc09d11f5eff93 --- acme/acme.go | 428 +++-------------- acme/acme_test.go | 651 +------------------------- acme/autocert/autocert.go | 100 +--- acme/autocert/autocert_test.go | 299 ++---------- acme/autocert/internal/acmetest/ca.go | 123 ++++- acme/autocert/renewal_test.go | 80 +--- acme/http_test.go | 6 +- acme/internal/acmeprobe/prober.go | 49 +- acme/jws.go | 3 + acme/rfc8555_test.go | 2 +- acme/types.go | 8 - 11 files changed, 259 insertions(+), 1490 deletions(-) diff --git a/acme/acme.go b/acme/acme.go index 73b19ef35a..c5676280fb 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -3,17 +3,21 @@ // license that can be found in the LICENSE file. // Package acme provides an implementation of the -// Automatic Certificate Management Environment (ACME) spec. -// The initial implementation was based on ACME draft-02 and -// is now being extended to comply with RFC 8555. -// See https://tools.ietf.org/html/draft-ietf-acme-acme-02 -// and https://tools.ietf.org/html/rfc8555 for details. +// Automatic Certificate Management Environment (ACME) spec, +// most famously used by LetsEncrypt. +// +// The initial implementation of this package was based on an early version +// of the spec. The current implementation supports only the modern +// RFC 8555 but some of the old API surface remains for compatibility. +// While code using the old API will still compile, it won't actually +// work because LetsEncrypt has removed support for the pre-RFC ACME +// spec. Note the deprecation comments to update your code. +// +// See https://tools.ietf.org/html/rfc8555 for the spec. // // 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 ( @@ -33,8 +37,6 @@ import ( "encoding/pem" "errors" "fmt" - "io" - "io/ioutil" "math/big" "net/http" "strings" @@ -72,6 +74,7 @@ const ( ) // Client is an ACME client. +// // The only required field is Key. An example of creating a client with a new key // is as follows: // @@ -143,9 +146,6 @@ type Client struct { func (c *Client) accountKID(ctx context.Context) keyID { c.cacheMu.Lock() defer c.cacheMu.Unlock() - if !c.dir.rfcCompliant() { - return noKeyID - } if c.kid != noKeyID { return c.kid } @@ -157,6 +157,8 @@ func (c *Client) accountKID(ctx context.Context) keyID { return c.kid } +var errPreRFC = errors.New("acme: server does not support the RFC 8555 version of ACME") + // Discover performs ACME server discovery using c.DirectoryURL. // // It caches successful result. So, subsequent calls will not result in @@ -177,53 +179,36 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) { c.addNonce(res.Header) var v struct { - Reg string `json:"new-reg"` - RegRFC string `json:"newAccount"` - Authz string `json:"new-authz"` - AuthzRFC string `json:"newAuthz"` - OrderRFC string `json:"newOrder"` - Cert string `json:"new-cert"` - Revoke string `json:"revoke-cert"` - RevokeRFC string `json:"revokeCert"` - NonceRFC string `json:"newNonce"` - KeyChangeRFC string `json:"keyChange"` - Meta struct { - Terms string `json:"terms-of-service"` - TermsRFC string `json:"termsOfService"` - WebsiteRFC string `json:"website"` - CAA []string `json:"caa-identities"` - CAARFC []string `json:"caaIdentities"` - ExternalAcctRFC bool `json:"externalAccountRequired"` + Reg string `json:"newAccount"` + Authz string `json:"newAuthz"` + Order string `json:"newOrder"` + Revoke string `json:"revokeCert"` + Nonce string `json:"newNonce"` + KeyChange string `json:"keyChange"` + Meta struct { + Terms string `json:"termsOfService"` + Website string `json:"website"` + CAA []string `json:"caaIdentities"` + ExternalAcct bool `json:"externalAccountRequired"` } } if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return Directory{}, err } - if v.OrderRFC == "" { - // Non-RFC compliant ACME CA. - c.dir = &Directory{ - RegURL: v.Reg, - AuthzURL: v.Authz, - CertURL: v.Cert, - RevokeURL: v.Revoke, - Terms: v.Meta.Terms, - Website: v.Meta.WebsiteRFC, - CAA: v.Meta.CAA, - } - return *c.dir, nil + if v.Order == "" { + return Directory{}, errPreRFC } - // RFC compliant ACME CA. c.dir = &Directory{ - RegURL: v.RegRFC, - AuthzURL: v.AuthzRFC, - OrderURL: v.OrderRFC, - RevokeURL: v.RevokeRFC, - NonceURL: v.NonceRFC, - KeyChangeURL: v.KeyChangeRFC, - Terms: v.Meta.TermsRFC, - Website: v.Meta.WebsiteRFC, - CAA: v.Meta.CAARFC, - ExternalAccountRequired: v.Meta.ExternalAcctRFC, + RegURL: v.Reg, + AuthzURL: v.Authz, + OrderURL: v.Order, + RevokeURL: v.Revoke, + NonceURL: v.Nonce, + KeyChangeURL: v.KeyChange, + Terms: v.Meta.Terms, + Website: v.Meta.Website, + CAA: v.Meta.CAA, + ExternalAccountRequired: v.Meta.ExternalAcct, } return *c.dir, nil } @@ -235,55 +220,11 @@ func (c *Client) directoryURL() string { return LetsEncryptURL } -// CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format. -// It is incompatible with RFC 8555. Callers should use CreateOrderCert when interfacing -// with an RFC-compliant CA. +// CreateCert was part of the old version of ACME. It is incompatible with RFC 8555. // -// The exp argument indicates the desired certificate validity duration. CA may issue a certificate -// with a different duration. -// If the bundle argument is true, the returned value will also contain the CA (issuer) certificate chain. -// -// In the case where CA server does not provide the issued certificate in the response, -// CreateCert will poll certURL using c.FetchCert, which will result in additional round-trips. -// In such a scenario, the caller can cancel the polling with ctx. -// -// CreateCert returns an error if the CA's response or chain was unreasonably large. -// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features. +// Deprecated: this was for the pre-RFC 8555 version of ACME. Callers should use CreateOrderCert. func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) { - if _, err := c.Discover(ctx); err != nil { - return nil, "", err - } - - req := struct { - Resource string `json:"resource"` - CSR string `json:"csr"` - NotBefore string `json:"notBefore,omitempty"` - NotAfter string `json:"notAfter,omitempty"` - }{ - Resource: "new-cert", - CSR: base64.RawURLEncoding.EncodeToString(csr), - } - now := timeNow() - req.NotBefore = now.Format(time.RFC3339) - if exp > 0 { - req.NotAfter = now.Add(exp).Format(time.RFC3339) - } - - res, err := c.post(ctx, nil, c.dir.CertURL, req, wantStatus(http.StatusCreated)) - if err != nil { - return nil, "", err - } - defer res.Body.Close() - - curl := res.Header.Get("Location") // cert permanent URL - if res.ContentLength == 0 { - // no cert in the body; poll until we get it - cert, err := c.FetchCert(ctx, curl, bundle) - return cert, curl, err - } - // slurp issued cert and CA chain, if requested - cert, err := c.responseCert(ctx, res, bundle) - return cert, curl, err + return nil, "", errPreRFC } // FetchCert retrieves already issued certificate from the given url, in DER format. @@ -297,20 +238,10 @@ func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, // Callers are encouraged to parse the returned value to ensure the certificate is valid // and has expected features. func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) { - dir, err := c.Discover(ctx) - if err != nil { - return nil, err - } - if dir.rfcCompliant() { - return c.fetchCertRFC(ctx, url, bundle) - } - - // Legacy non-authenticated GET request. - res, err := c.get(ctx, url, wantStatus(http.StatusOK)) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - return c.responseCert(ctx, res, bundle) + return c.fetchCertRFC(ctx, url, bundle) } // RevokeCert revokes a previously issued certificate cert, provided in DER format. @@ -320,30 +251,10 @@ func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]by // For instance, the key pair of the certificate may be authorized. // If the key is nil, c.Key is used instead. func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error { - dir, err := c.Discover(ctx) - if err != nil { - return err - } - if dir.rfcCompliant() { - return c.revokeCertRFC(ctx, key, cert, reason) - } - - // Legacy CA. - body := &struct { - Resource string `json:"resource"` - Cert string `json:"certificate"` - Reason int `json:"reason"` - }{ - Resource: "revoke-cert", - Cert: base64.RawURLEncoding.EncodeToString(cert), - Reason: int(reason), - } - res, err := c.post(ctx, key, dir.RevokeURL, body, wantStatus(http.StatusOK)) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return err } - defer res.Body.Close() - return nil + return c.revokeCertRFC(ctx, key, cert, reason) } // AcceptTOS always returns true to indicate the acceptance of a CA's Terms of Service @@ -366,75 +277,33 @@ func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL if c.Key == nil { return nil, errors.New("acme: client.Key must be set to Register") } - - dir, err := c.Discover(ctx) - if err != nil { - return nil, err - } - if dir.rfcCompliant() { - return c.registerRFC(ctx, acct, prompt) - } - - // Legacy ACME draft registration flow. - a, err := c.doReg(ctx, dir.RegURL, "new-reg", acct) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - var accept bool - if a.CurrentTerms != "" && a.CurrentTerms != a.AgreedTerms { - accept = prompt(a.CurrentTerms) - } - if accept { - a.AgreedTerms = a.CurrentTerms - a, err = c.UpdateReg(ctx, a) - } - return a, err + return c.registerRFC(ctx, acct, prompt) } // GetReg retrieves an existing account associated with c.Key. // -// The url argument is an Account URI used with pre-RFC 8555 CAs. -// It is ignored when interfacing with an RFC-compliant CA. +// The url argument is a legacy artifact of the pre-RFC 8555 API +// and is ignored. func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) { - dir, err := c.Discover(ctx) - if err != nil { - return nil, err - } - if dir.rfcCompliant() { - return c.getRegRFC(ctx) - } - - // Legacy CA. - a, err := c.doReg(ctx, url, "reg", nil) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - a.URI = url - return a, nil + return c.getRegRFC(ctx) } // UpdateReg updates an existing registration. // It returns an updated account copy. The provided account is not modified. // -// When interfacing with RFC-compliant CAs, a.URI is ignored and the account URL -// associated with c.Key is used instead. +// The account's URI is ignored and the account URL associated with +// c.Key is used instead. func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error) { - dir, err := c.Discover(ctx) - if err != nil { - return nil, err - } - if dir.rfcCompliant() { - return c.updateRegRFC(ctx, acct) - } - - // Legacy CA. - uri := acct.URI - a, err := c.doReg(ctx, uri, "reg", acct) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - a.URI = uri - return a, nil + return c.updateRegRFC(ctx, acct) } // Authorize performs the initial step in the pre-authorization flow, @@ -503,17 +372,11 @@ func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization // 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) { - dir, err := c.Discover(ctx) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - var res *http.Response - if dir.rfcCompliant() { - res, err = c.postAsGet(ctx, url, wantStatus(http.StatusOK)) - } else { - res, err = c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) - } + res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK)) if err != nil { return nil, err } @@ -535,7 +398,6 @@ func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorizati // // It does not revoke existing certificates. func (c *Client) RevokeAuthorization(ctx context.Context, url string) error { - // Required for c.accountKID() when in RFC mode. if _, err := c.Discover(ctx); err != nil { return err } @@ -565,18 +427,11 @@ func (c *Client) RevokeAuthorization(ctx context.Context, url string) error { // In all other cases WaitAuthorization returns an error. // If the Status is StatusInvalid, the returned error is of type *AuthorizationError. func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) { - // Required for c.accountKID() when in RFC mode. - dir, err := c.Discover(ctx) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - getfn := c.postAsGet - if !dir.rfcCompliant() { - getfn = c.get - } - for { - res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) if err != nil { return nil, err } @@ -619,17 +474,11 @@ func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorizat // // A client typically polls a challenge status using this method. func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) { - // Required for c.accountKID() when in RFC mode. - dir, err := c.Discover(ctx) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - getfn := c.postAsGet - if !dir.rfcCompliant() { - getfn = c.get - } - res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) if err != nil { return nil, err } @@ -647,29 +496,11 @@ func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, erro // // The server will then perform the validation asynchronously. func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error) { - // Required for c.accountKID() when in RFC mode. - dir, err := c.Discover(ctx) - if err != nil { + if _, err := c.Discover(ctx); err != nil { return nil, err } - var req interface{} = json.RawMessage("{}") // RFC-compliant CA - if !dir.rfcCompliant() { - auth, err := keyAuth(c.Key.Public(), chal.Token) - if err != nil { - return nil, err - } - req = struct { - Resource string `json:"resource"` - Type string `json:"type"` - Auth string `json:"keyAuthorization"` - }{ - Resource: "challenge", - Type: chal.Type, - Auth: auth, - } - } - res, err := c.post(ctx, nil, chal.URI, req, wantStatus( + res, err := c.post(ctx, nil, chal.URI, json.RawMessage("{}"), wantStatus( http.StatusOK, // according to the spec http.StatusAccepted, // Let's Encrypt: see https://goo.gl/WsJ7VT (acme-divergences.md) )) @@ -720,7 +551,7 @@ func (c *Client) HTTP01ChallengePath(token string) string { // TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response. // -// Deprecated: This challenge type is unused in both draft-02 and RFC versions of ACME spec. +// Deprecated: This challenge type is unused in both draft-02 and RFC versions of the ACME spec. func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) { ka, err := keyAuth(c.Key.Public(), token) if err != nil { @@ -738,7 +569,7 @@ func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tl // TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response. // -// Deprecated: This challenge type is unused in both draft-02 and RFC versions of ACME spec. +// Deprecated: This challenge type is unused in both draft-02 and RFC versions of the ACME spec. func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) { b := sha256.Sum256([]byte(token)) h := hex.EncodeToString(b[:]) @@ -805,63 +636,6 @@ func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) return tlsChallengeCert([]string{domain}, newOpt) } -// doReg sends all types of registration requests the old way (pre-RFC world). -// The type of request is identified by typ argument, which is a "resource" -// in the ACME spec terms. -// -// A non-nil acct argument indicates whether the intention is to mutate data -// of the Account. Only Contact and Agreement of its fields are used -// in such cases. -func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Account) (*Account, error) { - req := struct { - Resource string `json:"resource"` - Contact []string `json:"contact,omitempty"` - Agreement string `json:"agreement,omitempty"` - }{ - Resource: typ, - } - if acct != nil { - req.Contact = acct.Contact - req.Agreement = acct.AgreedTerms - } - res, err := c.post(ctx, nil, url, req, wantStatus( - http.StatusOK, // updates and deletes - http.StatusCreated, // new account creation - http.StatusAccepted, // Let's Encrypt divergent implementation - )) - if err != nil { - return nil, err - } - defer res.Body.Close() - - var v struct { - Contact []string - Agreement string - Authorizations string - Certificates string - } - if err := json.NewDecoder(res.Body).Decode(&v); err != nil { - return nil, fmt.Errorf("acme: invalid response: %v", err) - } - var tos string - if v := linkHeader(res.Header, "terms-of-service"); len(v) > 0 { - tos = v[0] - } - var authz string - if v := linkHeader(res.Header, "next"); len(v) > 0 { - authz = v[0] - } - return &Account{ - URI: res.Header.Get("Location"), - Contact: v.Contact, - AgreedTerms: v.Agreement, - CurrentTerms: tos, - Authz: authz, - Authorizations: v.Authorizations, - Certificates: v.Certificates, - }, nil -} - // popNonce returns a nonce value previously stored with c.addNonce // or fetches a fresh one from c.dir.NonceURL. // If NonceURL is empty, it first tries c.directoryURL() and, failing that, @@ -936,78 +710,6 @@ func nonceFromHeader(h http.Header) string { return h.Get("Replay-Nonce") } -func (c *Client) responseCert(ctx context.Context, res *http.Response, bundle bool) ([][]byte, error) { - b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1)) - if err != nil { - return nil, fmt.Errorf("acme: response stream: %v", err) - } - if len(b) > maxCertSize { - return nil, errors.New("acme: certificate is too big") - } - cert := [][]byte{b} - if !bundle { - return cert, nil - } - - // Append CA chain cert(s). - // At least one is required according to the spec: - // https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-6.3.1 - up := linkHeader(res.Header, "up") - if len(up) == 0 { - return nil, errors.New("acme: rel=up link not found") - } - if len(up) > maxChainLen { - return nil, errors.New("acme: rel=up link is too large") - } - for _, url := range up { - cc, err := c.chainCert(ctx, url, 0) - if err != nil { - return nil, err - } - cert = append(cert, cc...) - } - return cert, nil -} - -// chainCert fetches CA certificate chain recursively by following "up" links. -// Each recursive call increments the depth by 1, resulting in an error -// if the recursion level reaches maxChainLen. -// -// First chainCert call starts with depth of 0. -func (c *Client) chainCert(ctx context.Context, url string, depth int) ([][]byte, error) { - if depth >= maxChainLen { - return nil, errors.New("acme: certificate chain is too deep") - } - - res, err := c.get(ctx, url, wantStatus(http.StatusOK)) - if err != nil { - return nil, err - } - defer res.Body.Close() - b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1)) - if err != nil { - return nil, err - } - if len(b) > maxCertSize { - return nil, errors.New("acme: certificate is too big") - } - chain := [][]byte{b} - - uplink := linkHeader(res.Header, "up") - if len(uplink) > maxChainLen { - return nil, errors.New("acme: certificate chain is too large") - } - for _, up := range uplink { - cc, err := c.chainCert(ctx, up, depth+1) - if err != nil { - return nil, err - } - chain = append(chain, cc...) - } - - return chain, nil -} - // linkHeader returns URI-Reference values of all Link headers // with relation-type rel. // See https://tools.ietf.org/html/rfc5988#section-5 for details. @@ -1098,5 +800,5 @@ func encodePEM(typ string, b []byte) []byte { return pem.EncodeToMemory(pb) } -// timeNow is useful for testing for fixed current time. +// timeNow is time.Now, except in tests which can mess with it. var timeNow = time.Now diff --git a/acme/acme_test.go b/acme/acme_test.go index db9718a2a9..203c486c82 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -79,115 +79,6 @@ func decodeJWSHead(r io.Reader) (*jwsHead, error) { return &head, nil } -func TestDiscover(t *testing.T) { - const ( - reg = "https://example.com/acme/new-reg" - authz = "https://example.com/acme/new-authz" - cert = "https://example.com/acme/new-cert" - revoke = "https://example.com/acme/revoke-cert" - ) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Replay-Nonce", "testnonce") - fmt.Fprintf(w, `{ - "new-reg": %q, - "new-authz": %q, - "new-cert": %q, - "revoke-cert": %q - }`, reg, authz, cert, revoke) - })) - defer ts.Close() - c := Client{DirectoryURL: ts.URL} - dir, err := c.Discover(context.Background()) - if err != nil { - t.Fatal(err) - } - if dir.RegURL != reg { - t.Errorf("dir.RegURL = %q; want %q", dir.RegURL, reg) - } - if dir.AuthzURL != authz { - t.Errorf("dir.AuthzURL = %q; want %q", dir.AuthzURL, authz) - } - if dir.CertURL != cert { - t.Errorf("dir.CertURL = %q; want %q", dir.CertURL, cert) - } - if dir.RevokeURL != revoke { - t.Errorf("dir.RevokeURL = %q; want %q", dir.RevokeURL, revoke) - } - if _, exist := c.nonces["testnonce"]; !exist { - t.Errorf("c.nonces = %q; want 'testnonce' in the map", c.nonces) - } -} - -func TestRegister(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 { - Resource string - Contact []string - Agreement string - } - decodeJWSRequest(t, &j, r.Body) - - // Test request - if j.Resource != "new-reg" { - t.Errorf("j.Resource = %q; want new-reg", j.Resource) - } - if !reflect.DeepEqual(j.Contact, contacts) { - t.Errorf("j.Contact = %v; want %v", j.Contact, contacts) - } - - w.Header().Set("Location", "https://ca.tld/acme/reg/1") - w.Header().Set("Link", `;rel="next"`) - w.Header().Add("Link", `;rel="recover"`) - w.Header().Add("Link", `;rel="terms-of-service"`) - w.WriteHeader(http.StatusCreated) - b, _ := json.Marshal(contacts) - fmt.Fprintf(w, `{"contact": %s}`, b) - })) - defer ts.Close() - - prompt := func(url string) bool { - const terms = "https://ca.tld/acme/terms" - if url != terms { - t.Errorf("prompt url = %q; want %q", url, terms) - } - return false - } - - c := Client{ - Key: testKeyEC, - DirectoryURL: ts.URL, - dir: &Directory{RegURL: ts.URL}, - } - a := &Account{Contact: contacts} - var err error - if a, err = c.Register(context.Background(), a, prompt); err != nil { - t.Fatal(err) - } - if a.URI != "https://ca.tld/acme/reg/1" { - t.Errorf("a.URI = %q; want https://ca.tld/acme/reg/1", a.URI) - } - if a.Authz != "https://ca.tld/acme/new-authz" { - t.Errorf("a.Authz = %q; want https://ca.tld/acme/new-authz", a.Authz) - } - if a.CurrentTerms != "https://ca.tld/acme/terms" { - t.Errorf("a.CurrentTerms = %q; want https://ca.tld/acme/terms", a.CurrentTerms) - } - if !reflect.DeepEqual(a.Contact, contacts) { - t.Errorf("a.Contact = %v; want %v", a.Contact, contacts) - } -} - func TestRegisterWithoutKey(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "HEAD" { @@ -213,134 +104,6 @@ func TestRegisterWithoutKey(t *testing.T) { } } -func TestUpdateReg(t *testing.T) { - const terms = "https://ca.tld/acme/terms" - 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 { - Resource string - Contact []string - Agreement string - } - decodeJWSRequest(t, &j, r.Body) - - // Test request - if j.Resource != "reg" { - t.Errorf("j.Resource = %q; want reg", j.Resource) - } - if j.Agreement != terms { - t.Errorf("j.Agreement = %q; want %q", j.Agreement, terms) - } - if !reflect.DeepEqual(j.Contact, contacts) { - t.Errorf("j.Contact = %v; want %v", j.Contact, contacts) - } - - w.Header().Set("Link", `;rel="next"`) - w.Header().Add("Link", `;rel="recover"`) - w.Header().Add("Link", fmt.Sprintf(`<%s>;rel="terms-of-service"`, terms)) - w.WriteHeader(http.StatusOK) - b, _ := json.Marshal(contacts) - fmt.Fprintf(w, `{"contact":%s, "agreement":%q}`, b, terms) - })) - defer ts.Close() - - c := Client{ - Key: testKeyEC, - DirectoryURL: ts.URL, // don't dial outside of localhost - dir: &Directory{}, // don't do discovery - } - a := &Account{URI: ts.URL, Contact: contacts, AgreedTerms: terms} - var err error - if a, err = c.UpdateReg(context.Background(), a); err != nil { - t.Fatal(err) - } - if a.Authz != "https://ca.tld/acme/new-authz" { - t.Errorf("a.Authz = %q; want https://ca.tld/acme/new-authz", a.Authz) - } - if a.AgreedTerms != terms { - t.Errorf("a.AgreedTerms = %q; want %q", a.AgreedTerms, terms) - } - if a.CurrentTerms != terms { - t.Errorf("a.CurrentTerms = %q; want %q", a.CurrentTerms, terms) - } - if a.URI != ts.URL { - t.Errorf("a.URI = %q; want %q", a.URI, ts.URL) - } -} - -func TestGetReg(t *testing.T) { - const terms = "https://ca.tld/acme/terms" - const newTerms = "https://ca.tld/acme/new-terms" - 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 { - Resource string - Contact []string - Agreement string - } - decodeJWSRequest(t, &j, r.Body) - - // Test request - if j.Resource != "reg" { - t.Errorf("j.Resource = %q; want reg", j.Resource) - } - if len(j.Contact) != 0 { - t.Errorf("j.Contact = %v", j.Contact) - } - if j.Agreement != "" { - t.Errorf("j.Agreement = %q", j.Agreement) - } - - w.Header().Set("Link", `;rel="next"`) - w.Header().Add("Link", `;rel="recover"`) - w.Header().Add("Link", fmt.Sprintf(`<%s>;rel="terms-of-service"`, newTerms)) - w.WriteHeader(http.StatusOK) - b, _ := json.Marshal(contacts) - fmt.Fprintf(w, `{"contact":%s, "agreement":%q}`, b, terms) - })) - defer ts.Close() - - c := Client{ - Key: testKeyEC, - DirectoryURL: ts.URL, // don't dial outside of localhost - dir: &Directory{}, // don't do discovery - } - a, err := c.GetReg(context.Background(), ts.URL) - if err != nil { - t.Fatal(err) - } - if a.Authz != "https://ca.tld/acme/new-authz" { - t.Errorf("a.AuthzURL = %q; want https://ca.tld/acme/new-authz", a.Authz) - } - if a.AgreedTerms != terms { - t.Errorf("a.AgreedTerms = %q; want %q", a.AgreedTerms, terms) - } - if a.CurrentTerms != newTerms { - t.Errorf("a.CurrentTerms = %q; want %q", a.CurrentTerms, newTerms) - } - if a.URI != ts.URL { - t.Errorf("a.URI = %q; want %q", a.URI, ts.URL) - } -} - func TestAuthorize(t *testing.T) { tt := []struct{ typ, value string }{ {"dns", "example.com"}, @@ -491,82 +254,6 @@ func TestAuthorizeValid(t *testing.T) { } } -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", - "uri":"https://ca.tld/acme/challenge/publickey/id1", - "token":"token1" - }, - { - "type":"tls-sni-01", - "status":"pending", - "uri":"https://ca.tld/acme/challenge/publickey/id2", - "token":"token2" - } - ], - "combinations":[[0],[1]]}`) - })) - defer ts.Close() - - cl := Client{Key: testKeyEC, DirectoryURL: 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.URI != "https://ca.tld/acme/challenge/publickey/id1" { - t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", c.URI) - } - if c.Token != "token1" { - t.Errorf("c.Token = %q; want token1", c.Token) - } - - c = auth.Challenges[1] - if c.Type != "tls-sni-01" { - t.Errorf("c.Type = %q; want tls-sni-01", c.Type) - } - if c.URI != "https://ca.tld/acme/challenge/publickey/id2" { - t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id2", c.URI) - } - if c.Token != "token2" { - t.Errorf("c.Token = %q; want token2", c.Token) - } - - combs := [][]int{{0}, {1}} - if !reflect.DeepEqual(auth.Combinations, combs) { - t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs) - } -} - func TestWaitAuthorization(t *testing.T) { t.Run("wait loop", func(t *testing.T) { var count int @@ -678,9 +365,13 @@ func TestWaitAuthorization(t *testing.T) { } }) } + func runWaitAuthorization(ctx context.Context, t *testing.T, h http.HandlerFunc) (*Authorization, error) { t.Helper() - ts := httptest.NewServer(h) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Replay-Nonce", fmt.Sprintf("bad-test-nonce-%v", time.Now().UnixNano())) + h(w, r) + })) defer ts.Close() type res struct { authz *Authorization @@ -688,7 +379,12 @@ func runWaitAuthorization(ctx context.Context, t *testing.T, h http.HandlerFunc) } ch := make(chan res, 1) go func() { - var client = Client{DirectoryURL: ts.URL} + client := &Client{ + Key: testKey, + DirectoryURL: ts.URL, + dir: &Directory{}, + kid: "some-key-id", // set to avoid lookup attempt + } a, err := client.WaitAuthorization(ctx, ts.URL) ch <- res{a, err} }() @@ -743,236 +439,6 @@ func TestRevokeAuthorization(t *testing.T) { } } -func TestPollChallenge(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", - "uri":"https://ca.tld/acme/challenge/publickey/id1", - "token":"token1"}`) - })) - defer ts.Close() - - cl := Client{Key: testKeyEC, DirectoryURL: ts.URL} - 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.URI != "https://ca.tld/acme/challenge/publickey/id1" { - t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", chall.URI) - } - if chall.Token != "token1" { - t.Errorf("c.Token = %q; want token1", chall.Token) - } -} - -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 { - Resource string - Type string - Auth string `json:"keyAuthorization"` - } - decodeJWSRequest(t, &j, r.Body) - - // Test request - if j.Resource != "challenge" { - t.Errorf(`resource = %q; want "challenge"`, j.Resource) - } - if j.Type != "http-01" { - t.Errorf(`type = %q; want "http-01"`, j.Type) - } - keyAuth := "token1." + testKeyECThumbprint - if j.Auth != keyAuth { - t.Errorf(`keyAuthorization = %q; want %q`, j.Auth, keyAuth) - } - - // Respond to request - w.WriteHeader(http.StatusAccepted) - fmt.Fprintf(w, `{ - "type":"http-01", - "status":"pending", - "uri":"https://ca.tld/acme/challenge/publickey/id1", - "token":"token1", - "keyAuthorization":%q - }`, keyAuth) - })) - defer ts.Close() - - cl := Client{ - Key: testKeyEC, - DirectoryURL: ts.URL, // don't dial outside of localhost - dir: &Directory{}, // don't do discovery - } - c, err := cl.Accept(context.Background(), &Challenge{ - URI: ts.URL, - Token: "token1", - Type: "http-01", - }) - if err != nil { - t.Fatal(err) - } - - if c.Type != "http-01" { - t.Errorf("c.Type = %q; want http-01", c.Type) - } - if c.URI != "https://ca.tld/acme/challenge/publickey/id1" { - t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", c.URI) - } - if c.Token != "token1" { - t.Errorf("c.Token = %q; want token1", c.Token) - } -} - -func TestNewCert(t *testing.T) { - notBefore := time.Now() - notAfter := notBefore.AddDate(0, 2, 0) - timeNow = func() time.Time { return notBefore } - - 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 { - Resource string `json:"resource"` - CSR string `json:"csr"` - NotBefore string `json:"notBefore,omitempty"` - NotAfter string `json:"notAfter,omitempty"` - } - decodeJWSRequest(t, &j, r.Body) - - // Test request - if j.Resource != "new-cert" { - t.Errorf(`resource = %q; want "new-cert"`, j.Resource) - } - if j.NotBefore != notBefore.Format(time.RFC3339) { - t.Errorf(`notBefore = %q; wanted %q`, j.NotBefore, notBefore.Format(time.RFC3339)) - } - if j.NotAfter != notAfter.Format(time.RFC3339) { - t.Errorf(`notAfter = %q; wanted %q`, j.NotAfter, notAfter.Format(time.RFC3339)) - } - - // Respond to request - 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, - } - - 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", "https://ca.tld/acme/cert/1") - w.WriteHeader(http.StatusCreated) - w.Write(sampleCert) - })) - 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, dir: &Directory{CertURL: ts.URL}} - cert, certURL, err := c.CreateCert(context.Background(), csrb, notAfter.Sub(notBefore), false) - if err != nil { - t.Fatal(err) - } - if cert == nil { - t.Errorf("cert is nil") - } - if certURL != "https://ca.tld/acme/cert/1" { - t.Errorf("certURL = %q; want https://ca.tld/acme/cert/1", certURL) - } -} - -func TestFetchCert(t *testing.T) { - var count byte - var ts *httptest.Server - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - count++ - if count < 3 { - up := fmt.Sprintf("<%s>;rel=up", ts.URL) - w.Header().Set("Link", up) - } - w.Write([]byte{count}) - })) - defer ts.Close() - cl := newTestClient() - res, err := cl.FetchCert(context.Background(), ts.URL, true) - if err != nil { - t.Fatalf("FetchCert: %v", err) - } - cert := [][]byte{{1}, {2}, {3}} - if !reflect.DeepEqual(res, cert) { - t.Errorf("res = %v; want %v", res, cert) - } -} - -func TestFetchCertRetry(t *testing.T) { - var count int - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if count < 1 { - w.Header().Set("Retry-After", "0") - w.WriteHeader(http.StatusTooManyRequests) - count++ - return - } - w.Write([]byte{1}) - })) - defer ts.Close() - cl := newTestClient() - res, err := cl.FetchCert(context.Background(), ts.URL, false) - if err != nil { - t.Fatalf("FetchCert: %v", err) - } - cert := [][]byte{{1}} - if !reflect.DeepEqual(res, cert) { - t.Errorf("res = %v; want %v", res, cert) - } -} - func TestFetchCertCancel(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Retry-After", "0") @@ -1043,42 +509,6 @@ func TestFetchCertSize(t *testing.T) { } } -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 { - Resource string - Certificate string - Reason int - } - decodeJWSRequest(t, &req, r.Body) - if req.Resource != "revoke-cert" { - t.Errorf("req.Resource = %q; want revoke-cert", req.Resource) - } - 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, - dir: &Directory{RevokeURL: 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"}}) @@ -1199,65 +629,6 @@ func TestNonce_popWhenEmpty(t *testing.T) { } } -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.Authorize happy; we're not testing its result. - defer func() { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"status":"valid"}`)) - }() - - head, err := decodeJWSHead(r.Body) - 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, - DirectoryURL: ts.URL, // nonces are fetched from here first - dir: &Directory{AuthzURL: ts.URL}, - } - if _, err := client.Authorize(context.Background(), "example.com"); err != nil { - t.Errorf("client.Authorize 1: %v", err) - } - // The second call should not generate another extra HEAD request. - if _, err := client.Authorize(context.Background(), "example.com"); err != nil { - t.Errorf("client.Authorize 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 TestLinkHeader(t *testing.T) { h := http.Header{"Link": { `;rel="next"`, diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index c7fbc54c45..2fe99e1465 100644 --- a/acme/autocert/autocert.go +++ b/acme/autocert/autocert.go @@ -47,6 +47,8 @@ var createCertRetryAfter = time.Minute // pseudoRand is safe for concurrent use. var pseudoRand *lockedMathRand +var errPreRFC = errors.New("autocert: ACME server doesn't support RFC 8555") + func init() { src := mathrand.NewSource(time.Now().UnixNano()) pseudoRand = &lockedMathRand{rnd: mathrand.New(src)} @@ -658,99 +660,25 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, ck cert if err != nil { return nil, nil, err } + if dir.OrderURL == "" { + return nil, nil, errPreRFC + } - var chain [][]byte - switch { - // Pre-RFC legacy CA. - case dir.OrderURL == "": - if err := m.verify(ctx, client, ck.domain); err != nil { - return nil, nil, err - } - der, _, err := client.CreateCert(ctx, csr, 0, true) - if err != nil { - return nil, nil, err - } - chain = der - // RFC 8555 compliant CA. - default: - o, err := m.verifyRFC(ctx, client, ck.domain) - if err != nil { - return nil, nil, err - } - der, _, err := client.CreateOrderCert(ctx, o.FinalizeURL, csr, true) - if err != nil { - return nil, nil, err - } - chain = der + o, err := m.verifyRFC(ctx, client, ck.domain) + if err != nil { + return nil, nil, err } - leaf, err = validCert(ck, chain, key, m.now()) + der, _, err = client.CreateOrderCert(ctx, o.FinalizeURL, csr, true) if err != nil { return nil, nil, err } - return chain, leaf, nil -} - -// verify runs the identifier (domain) pre-authorization flow for legacy CAs -// using each applicable ACME challenge type. -func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string) error { - // Remove all hanging authorizations to reduce rate limit quotas - // after we're done. - var authzURLs []string - defer func() { - go m.deactivatePendingAuthz(authzURLs) - }() - - // errs accumulates challenge failure errors, printed if all fail - errs := make(map[*acme.Challenge]error) - challengeTypes := m.supportedChallengeTypes() - var nextTyp int // challengeType index of the next challenge type to try - for { - // Start domain authorization and get the challenge. - authz, err := client.Authorize(ctx, domain) - if err != nil { - return err - } - authzURLs = append(authzURLs, authz.URI) - // No point in accepting challenges if the authorization status - // is in a final state. - switch authz.Status { - case acme.StatusValid: - return nil // already authorized - case acme.StatusInvalid: - return fmt.Errorf("acme/autocert: invalid authorization %q", authz.URI) - } - - // Pick the next preferred challenge. - var chal *acme.Challenge - for chal == nil && nextTyp < len(challengeTypes) { - chal = pickChallenge(challengeTypes[nextTyp], authz.Challenges) - nextTyp++ - } - if chal == nil { - errorMsg := fmt.Sprintf("acme/autocert: unable to authorize %q", domain) - for chal, err := range errs { - errorMsg += fmt.Sprintf("; challenge %q failed with error: %v", chal.Type, err) - } - return errors.New(errorMsg) - } - cleanup, err := m.fulfill(ctx, client, chal, domain) - if err != nil { - errs[chal] = err - continue - } - defer cleanup() - if _, err := client.Accept(ctx, chal); err != nil { - errs[chal] = err - continue - } + chain := der - // A challenge is fulfilled and accepted: wait for the CA to validate. - if _, err := client.WaitAuthorization(ctx, authz.URI); err != nil { - errs[chal] = err - continue - } - return nil + leaf, err = validCert(ck, chain, key, m.now()) + if err != nil { + return nil, nil, err } + return chain, leaf, nil } // verifyRFC runs the identifier (domain) order-based authorization flow for RFC compliant CAs diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go index 59f39c1c16..9a076ebda3 100644 --- a/acme/autocert/autocert_test.go +++ b/acme/autocert/autocert_test.go @@ -18,6 +18,7 @@ import ( "encoding/asn1" "encoding/base64" "encoding/json" + "errors" "fmt" "html/template" "io" @@ -473,100 +474,33 @@ type getCertificateFunc func(domain string) (*tls.Certificate, error) // startACMEServerStub runs an ACME server // The domain argument is the expected domain name of a certificate request. -// TODO: Drop this in favour of x/crypto/acme/autocert/internal/acmetest. func startACMEServerStub(t *testing.T, tokenCert getCertificateFunc, domain string) (url string, finish func()) { - verifyTokenCert := func() { - tlscert, err := tokenCert(domain) - if err != nil { - t.Errorf("verifyTokenCert: tokenCert(%q): %v", domain, err) - return - } - crt, err := x509.ParseCertificate(tlscert.Certificate[0]) - if err != nil { - t.Errorf("verifyTokenCert: x509.ParseCertificate: %v", err) - } - if err := crt.VerifyHostname(domain); err != nil { - t.Errorf("verifyTokenCert: %v", err) - } - // See https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1 - oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} - for _, x := range crt.Extensions { - if x.Id.Equal(oid) { - // No need to check the extension value here. - // This is done in acme package tests. - return + // ACME CA server stub + ca := acmetest.NewCAServer([]string{"tls-alpn-01"}, nil) + + us := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + })) + us.TLS = &tls.Config{ + NextProtos: []string{"http/1.1", acme.ALPNProto}, + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if hello.ServerName != domain { + return nil, errors.New("server name did not match domain") } - } - t.Error("verifyTokenCert: no id-pe-acmeIdentifier extension found") + + return tokenCert(domain) + }, } + us.StartTLS() - // ACME CA server stub - var ca *httptest.Server - ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Replay-Nonce", "nonce") - if r.Method == "HEAD" { - // a nonce request - return - } + // In TLS-ALPN challenge verification, CA connects to the domain:443 in question. + // Because the domain won't resolve in tests, we need to tell the CA + // where to dial to instead. + ca.Resolve(domain, strings.TrimPrefix(us.URL, "https://")) - switch r.URL.Path { - // discovery - case "/": - if err := discoTmpl.Execute(w, ca.URL); err != nil { - t.Errorf("discoTmpl: %v", err) - } - // client key registration - case "/new-reg": - w.Write([]byte("{}")) - // domain authorization - case "/new-authz": - w.Header().Set("Location", ca.URL+"/authz/1") - w.WriteHeader(http.StatusCreated) - if err := authzTmpl.Execute(w, ca.URL); err != nil { - t.Errorf("authzTmpl: %v", err) - } - // accept tls-alpn-01 challenge - case "/challenge/tls-alpn-01": - verifyTokenCert() - w.Write([]byte("{}")) - // authorization status - case "/authz/1": - w.Write([]byte(`{"status": "valid"}`)) - // cert request - case "/new-cert": - var req struct { - CSR string `json:"csr"` - } - decodePayload(&req, r.Body) - b, _ := base64.RawURLEncoding.DecodeString(req.CSR) - csr, err := x509.ParseCertificateRequest(b) - if err != nil { - t.Errorf("new-cert: CSR: %v", err) - } - if csr.Subject.CommonName != domain { - t.Errorf("CommonName in CSR = %q; want %q", csr.Subject.CommonName, domain) - } - der, err := dummyCert(csr.PublicKey, domain) - if err != nil { - t.Errorf("new-cert: dummyCert: %v", err) - } - chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL) - w.Header().Set("Link", chainUp) - w.WriteHeader(http.StatusCreated) - w.Write(der) - // CA chain cert - case "/ca-cert": - der, err := dummyCert(nil, "ca") - if err != nil { - t.Errorf("ca-cert: dummyCert: %v", err) - } - w.Write(der) - default: - t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) - } - })) finish = func() { ca.Close() + us.Close() // make sure token cert was removed cancel := make(chan struct{}) @@ -636,74 +570,7 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl } func TestVerifyHTTP01(t *testing.T) { - var ( - http01 http.Handler - - authzCount int // num. of created authorizations - didAcceptHTTP01 bool - ) - - verifyHTTPToken := func() { - r := httptest.NewRequest("GET", "/.well-known/acme-challenge/token-http-01", nil) - w := httptest.NewRecorder() - http01.ServeHTTP(w, r) - if w.Code != http.StatusOK { - t.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK) - } - if v := w.Body.String(); !strings.HasPrefix(v, "token-http-01.") { - t.Errorf("http token value = %q; want 'token-http-01.' prefix", v) - } - } - - // ACME CA server stub, only the needed bits. - // TODO: Replace this with x/crypto/acme/autocert/internal/acmetest. - var ca *httptest.Server - ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Replay-Nonce", "nonce") - if r.Method == "HEAD" { - // a nonce request - return - } - - switch r.URL.Path { - // Discovery. - case "/": - if err := discoTmpl.Execute(w, ca.URL); err != nil { - t.Errorf("discoTmpl: %v", err) - } - // Client key registration. - case "/new-reg": - w.Write([]byte("{}")) - // New domain authorization. - case "/new-authz": - authzCount++ - w.Header().Set("Location", fmt.Sprintf("%s/authz/%d", ca.URL, authzCount)) - w.WriteHeader(http.StatusCreated) - if err := authzTmpl.Execute(w, ca.URL); err != nil { - t.Errorf("authzTmpl: %v", err) - } - // Reject tls-alpn-01. - case "/challenge/tls-alpn-01": - http.Error(w, "won't accept tls-sni-01", http.StatusBadRequest) - // Should not accept dns-01. - case "/challenge/dns-01": - t.Errorf("dns-01 challenge was accepted") - http.Error(w, "won't accept dns-01", http.StatusBadRequest) - // Accept http-01. - case "/challenge/http-01": - didAcceptHTTP01 = true - verifyHTTPToken() - w.Write([]byte("{}")) - // Authorization statuses. - case "/authz/1": // tls-alpn-01 - w.Write([]byte(`{"status": "invalid"}`)) - case "/authz/2": // http-01 - w.Write([]byte(`{"status": "valid"}`)) - default: - http.NotFound(w, r) - t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) - } - })) + ca := acmetest.NewCAServer([]string{"http-01"}, []string{"example.org"}) defer ca.Close() m := &Manager{ @@ -711,129 +578,47 @@ func TestVerifyHTTP01(t *testing.T) { DirectoryURL: ca.URL, }, } - http01 = m.HTTPHandler(nil) + + srv := httptest.NewServer(m.HTTPHandler(nil)) + defer srv.Close() + + ca.Resolve("example.org", strings.TrimPrefix(srv.URL, "http://")) + ctx := context.Background() client, err := m.acmeClient(ctx) if err != nil { t.Fatalf("m.acmeClient: %v", err) } - if err := m.verify(ctx, client, "example.org"); err != nil { + + if _, err := m.verifyRFC(ctx, client, "example.org"); err != nil { t.Errorf("m.verify: %v", err) } - // Only tls-alpn-01 and http-01 must be accepted. - // The dns-01 challenge is unsupported. - if authzCount != 2 { - t.Errorf("authzCount = %d; want 2", authzCount) - } - if !didAcceptHTTP01 { - t.Error("did not accept http-01 challenge") - } } func TestRevokeFailedAuthz(t *testing.T) { - // Prefill authorization URIs expected to be revoked. - // The challenges are selected in a specific order, - // each tried within a newly created authorization. - // This means each authorization URI corresponds to a different challenge type. - revokedAuthz := map[string]bool{ - "/authz/0": false, // tls-alpn-01 - "/authz/1": false, // http-01 - "/authz/2": false, // no viable challenge, but authz is created - } - - var authzCount int // num. of created authorizations - var revokeCount int // num. of revoked authorizations - done := make(chan struct{}) // closed when revokeCount is 3 - - // ACME CA server stub, only the needed bits. - // TODO: Replace this with x/crypto/acme/autocert/internal/acmetest. - var ca *httptest.Server - ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Replay-Nonce", "nonce") - if r.Method == "HEAD" { - // a nonce request - return - } - - switch r.URL.Path { - // Discovery. - case "/": - if err := discoTmpl.Execute(w, ca.URL); err != nil { - t.Errorf("discoTmpl: %v", err) - } - // Client key registration. - case "/new-reg": - w.Write([]byte("{}")) - // New domain authorization. - case "/new-authz": - w.Header().Set("Location", fmt.Sprintf("%s/authz/%d", ca.URL, authzCount)) - w.WriteHeader(http.StatusCreated) - if err := authzTmpl.Execute(w, ca.URL); err != nil { - t.Errorf("authzTmpl: %v", err) - } - authzCount++ - // tls-alpn-01 challenge "accept" request. - case "/challenge/tls-alpn-01": - // Refuse. - http.Error(w, "won't accept tls-alpn-01 challenge", http.StatusBadRequest) - // http-01 challenge "accept" request. - case "/challenge/http-01": - // Refuse. - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"status":"invalid"}`)) - // Authorization requests. - case "/authz/0", "/authz/1", "/authz/2": - // Revocation requests. - if r.Method == "POST" { - var req struct{ Status string } - if err := decodePayload(&req, r.Body); err != nil { - t.Errorf("%s: decodePayload: %v", r.URL, err) - } - switch req.Status { - case "deactivated": - revokedAuthz[r.URL.Path] = true - revokeCount++ - if revokeCount >= 3 { - // Last authorization is revoked. - defer close(done) - } - default: - t.Errorf("%s: req.Status = %q; want 'deactivated'", r.URL, req.Status) - } - w.Write([]byte(`{"status": "invalid"}`)) - return - } - // Authorization status requests. - w.Write([]byte(`{"status":"pending"}`)) - default: - http.NotFound(w, r) - t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) - } - })) + ca := acmetest.NewCAServer([]string{"tls-alpn-01", "dns-01", "http-01"}, []string{"unused"}) defer ca.Close() m := &Manager{ Client: &acme.Client{DirectoryURL: ca.URL}, } m.HTTPHandler(nil) // enable http-01 challenge type - // Should fail and revoke 3 authorizations. - // The first 2 are tls-alpn-01 and http-01 challenges. - // The third time an authorization is created but no viable challenge is found. - // See revokedAuthz above for more explanation. + + // Should fail and revoke the authorization. if _, err := m.createCert(context.Background(), exampleCertKey); err == nil { t.Errorf("m.createCert returned nil error") } - select { - case <-time.After(3 * time.Second): - t.Error("revocations took too long") - case <-done: - // revokeCount is at least 3. - } - for uri, ok := range revokedAuthz { - if !ok { - t.Errorf("%q authorization was not revoked", uri) + + for attempts := 0; attempts < 30; attempts++ { + auth, err := m.Client.GetAuthorization(context.Background(), ca.URL+"/authz/"+exampleDomain) + if err == nil && auth.Status == acme.StatusDeactivated { + return } + + time.Sleep(100 * time.Millisecond) } + + t.Error("revocations took too long") } func TestHTTPHandlerDefaultFallback(t *testing.T) { diff --git a/acme/autocert/internal/acmetest/ca.go b/acme/autocert/internal/acmetest/ca.go index faffd20bc9..358d293799 100644 --- a/acme/autocert/internal/acmetest/ca.go +++ b/acme/autocert/internal/acmetest/ca.go @@ -18,6 +18,7 @@ import ( "encoding/base64" "encoding/json" "encoding/pem" + "errors" "fmt" "io" "log" @@ -146,10 +147,10 @@ func (ca *CAServer) Resolve(domain, addr string) { } type discovery struct { - NewNonce string `json:"newNonce"` - NewReg string `json:"newAccount"` - NewOrder string `json:"newOrder"` - NewAuthz string `json:"newAuthz"` + NewNonce string `json:"newNonce"` + NewAccount string `json:"newAccount"` + NewOrder string `json:"newOrder"` + NewAuthz string `json:"newAuthz"` } type challenge struct { @@ -186,10 +187,10 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { // Discovery request. case r.URL.Path == "/": resp := &discovery{ - NewNonce: ca.serverURL("/new-nonce"), - NewReg: ca.serverURL("/new-reg"), - NewOrder: ca.serverURL("/new-order"), - NewAuthz: ca.serverURL("/new-authz"), + NewNonce: ca.serverURL("/new-nonce"), + NewAccount: ca.serverURL("/new-account"), + NewOrder: ca.serverURL("/new-order"), + NewAuthz: ca.serverURL("/new-authz"), } if err := json.NewEncoder(w).Encode(resp); err != nil { panic(fmt.Sprintf("discovery response: %v", err)) @@ -201,7 +202,7 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { return // Client key registration request. - case r.URL.Path == "/new-reg": + case r.URL.Path == "/new-account": // TODO: Check the user account key against a ca.accountKeys? w.Header().Set("Location", ca.serverURL("/accounts/1")) w.WriteHeader(http.StatusCreated) @@ -262,9 +263,32 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { panic(fmt.Sprintf("new authz response: %v", err)) } + // Accept http-01 challenge type requests + case strings.HasPrefix(r.URL.Path, "/challenge/http-01/"): + domain := strings.TrimPrefix(r.URL.Path, "/challenge/http-01/") + if err := ca.matchWhitelist([]string{domain}); err != nil { + ca.httpErrorf(w, http.StatusUnauthorized, `{"status":"invalid"}`) + return + } + + ca.mu.Lock() + _, exist := ca.authorizations[domain] + ca.mu.Unlock() + if !exist { + ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: no authz for %q", domain) + return + } + go ca.validateChallenge("http-01", domain) + w.Write([]byte("{}")) + // Accept tls-alpn-01 challenge type requests. case strings.HasPrefix(r.URL.Path, "/challenge/tls-alpn-01/"): domain := strings.TrimPrefix(r.URL.Path, "/challenge/tls-alpn-01/") + if err := ca.matchWhitelist([]string{domain}); err != nil { + ca.httpErrorf(w, http.StatusUnauthorized, `{"status":"invalid"}`) + return + } + ca.mu.Lock() _, exist := ca.authorizations[domain] ca.mu.Unlock() @@ -280,11 +304,22 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { domain := strings.TrimPrefix(r.URL.Path, "/authz/") ca.mu.Lock() defer ca.mu.Unlock() + authz, ok := ca.authorizations[domain] if !ok { ca.httpErrorf(w, http.StatusNotFound, "no authz for %q", domain) return } + + var req struct { + Status string + } + if err := decodePayload(&req, r.Body); err == nil { + if req.Status == acme.StatusDeactivated { + authz.Status = acme.StatusDeactivated + } + } + if err := json.NewEncoder(w).Encode(authz); err != nil { panic(fmt.Sprintf("get authz for %q response: %v", domain, err)) } @@ -387,6 +422,8 @@ func (ca *CAServer) storedOrder(i string) (*order, error) { if idx > len(ca.orders)-1 { return nil, fmt.Errorf("storedOrder: no such order %d", idx) } + + ca.updatePendingOrders() return ca.orders[idx], nil } @@ -451,6 +488,8 @@ func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err erro func (ca *CAServer) validateChallenge(typ, identifier string) { var err error switch typ { + case "http-01": + err = ca.verifyHTTPChallenge(identifier) case "tls-alpn-01": err = ca.verifyALPNChallenge(identifier) default: @@ -465,30 +504,25 @@ func (ca *CAServer) validateChallenge(typ, identifier string) { authz.Status = "valid" } log.Printf("validated %q for %q; authz status is now: %s", typ, identifier, authz.Status) + + ca.updatePendingOrders() +} + +func (ca *CAServer) updatePendingOrders() { // Update all pending orders. // An order becomes "ready" if all authorizations are "valid". // An order becomes "invalid" if any authorization is "invalid". // Status changes: https://tools.ietf.org/html/rfc8555#section-7.1.6 -OrdersLoop: for i, o := range ca.orders { if o.Status != acme.StatusPending { continue } - var countValid int - for _, zurl := range o.AuthzURLs { - z, ok := ca.authorizations[path.Base(zurl)] - if !ok { - log.Printf("no authz %q for order %d", zurl, i) - continue OrdersLoop - } - if z.Status == acme.StatusInvalid { - o.Status = acme.StatusInvalid - log.Printf("order %d is now invalid", i) - continue OrdersLoop - } - if z.Status == acme.StatusValid { - countValid++ - } + + countValid, countInvalid := ca.validateAuthzURLs(o.AuthzURLs) + if countInvalid > 0 { + o.Status = acme.StatusInvalid + log.Printf("order %d is now invalid", i) + continue } if countValid == len(o.AuthzURLs) { o.Status = acme.StatusReady @@ -498,6 +532,45 @@ OrdersLoop: } } +func (ca *CAServer) validateAuthzURLs(urls []string) (countValid, countInvalid int) { + for _, zurl := range urls { + z, ok := ca.authorizations[path.Base(zurl)] + if !ok { + continue + } + if z.Status == acme.StatusInvalid { + countInvalid++ + } + if z.Status == acme.StatusValid { + countValid++ + } + } + return countValid, countInvalid +} + +func (ca *CAServer) verifyHTTPChallenge(domain string) error { + addr, err := ca.addr(domain) + if err != nil { + return err + } + + ca.mu.Lock() + authz := ca.authorizations[domain] + ca.mu.Unlock() + + challenge := authz.Challenges[0] + resp, err := http.Get("http://" + addr + "/.well-known/acme-challenge/" + challenge.Token) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return errors.New("unexpected http-01 challenge response status") + } + + // TODO: verify response body + return nil +} + func (ca *CAServer) verifyALPNChallenge(domain string) error { const acmeALPNProto = "acme-tls/1" diff --git a/acme/autocert/renewal_test.go b/acme/autocert/renewal_test.go index d13d19041d..ef73c5862b 100644 --- a/acme/autocert/renewal_test.go +++ b/acme/autocert/renewal_test.go @@ -10,15 +10,14 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/tls" - "crypto/x509" - "encoding/base64" - "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert/internal/acmetest" ) func TestRenewalNext(t *testing.T) { @@ -48,62 +47,7 @@ func TestRenewalNext(t *testing.T) { } func TestRenewFromCache(t *testing.T) { - // ACME CA server stub - var ca *httptest.Server - ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Replay-Nonce", "nonce") - if r.Method == "HEAD" { - // a nonce request - return - } - - switch r.URL.Path { - // discovery - case "/": - if err := discoTmpl.Execute(w, ca.URL); err != nil { - t.Fatalf("discoTmpl: %v", err) - } - // client key registration - case "/new-reg": - w.Write([]byte("{}")) - // domain authorization - case "/new-authz": - w.Header().Set("Location", ca.URL+"/authz/1") - w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"status": "valid"}`)) - // authorization status request done by Manager's revokePendingAuthz. - case "/authz/1": - w.Write([]byte(`{"status": "valid"}`)) - // cert request - case "/new-cert": - var req struct { - CSR string `json:"csr"` - } - decodePayload(&req, r.Body) - b, _ := base64.RawURLEncoding.DecodeString(req.CSR) - csr, err := x509.ParseCertificateRequest(b) - if err != nil { - t.Fatalf("new-cert: CSR: %v", err) - } - der, err := dummyCert(csr.PublicKey, exampleDomain) - if err != nil { - t.Fatalf("new-cert: dummyCert: %v", err) - } - chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL) - w.Header().Set("Link", chainUp) - w.WriteHeader(http.StatusCreated) - w.Write(der) - // CA chain cert - case "/ca-cert": - der, err := dummyCert(nil, "ca") - if err != nil { - t.Fatalf("ca-cert: dummyCert: %v", err) - } - w.Write(der) - default: - t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) - } - })) + ca := acmetest.NewCAServer([]string{"tls-alpn-01"}, []string{exampleDomain}) defer ca.Close() man := &Manager{ @@ -116,6 +60,24 @@ func TestRenewFromCache(t *testing.T) { } defer man.stopRenew() + us := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + })) + us.TLS = &tls.Config{ + NextProtos: []string{"http/1.1", acme.ALPNProto}, + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + cert, err := man.GetCertificate(hello) + if err != nil { + t.Errorf("m.GetCertificate: %v", err) + } + return cert, err + }, + } + us.StartTLS() + defer us.Close() + + ca.Resolve(exampleDomain, strings.TrimPrefix(us.URL, "https://")) + // cache an almost expired cert key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { diff --git a/acme/http_test.go b/acme/http_test.go index cf1df36eb7..f35e04a985 100644 --- a/acme/http_test.go +++ b/acme/http_test.go @@ -115,8 +115,8 @@ func TestPostWithRetries(t *testing.T) { if _, err := client.Authorize(context.Background(), "example.com"); err != nil { t.Errorf("client.Authorize 1: %v", err) } - if count != 4 { - t.Errorf("total requests count: %d; want 4", count) + if count != 3 { + t.Errorf("total requests count: %d; want 3", count) } } @@ -224,7 +224,7 @@ func TestUserAgent(t *testing.T) { } w.WriteHeader(http.StatusOK) - w.Write([]byte(`{}`)) + w.Write([]byte(`{"newOrder": "sure"}`)) })) defer ts.Close() diff --git a/acme/internal/acmeprobe/prober.go b/acme/internal/acmeprobe/prober.go index 55d702b76a..471707de02 100644 --- a/acme/internal/acmeprobe/prober.go +++ b/acme/internal/acmeprobe/prober.go @@ -50,14 +50,13 @@ import ( var ( // ACME CA directory URL. - // Let's Encrypt v1 prod: https://acme-v01.api.letsencrypt.org/directory // Let's Encrypt v2 prod: https://acme-v02.api.letsencrypt.org/directory // Let's Encrypt v2 staging: https://acme-staging-v02.api.letsencrypt.org/directory // See the following for more CAs implementing ACME protocol: // https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment#CAs_&_PKIs_that_offer_ACME_certificates directory = flag.String("d", "", "ACME directory URL.") reginfo = flag.String("r", "", "ACME account registration info.") - flow = flag.String("f", "", "Flow to run: order, preauthz (RFC8555) or preauthz02 (draft-02).") + flow = flag.String("f", "", `Flow to run: "order" or "preauthz" (RFC8555).`) chaltyp = flag.String("t", "", "Challenge type: tls-alpn-01, http-01 or dns-01.") addr = flag.String("a", "", "Local server address for tls-alpn-01 and http-01.") dnsscript = flag.String("s", "", "Script to run for provisioning dns-01 challenges.") @@ -127,8 +126,6 @@ When running with dns-01 challenge type, use -s argument instead of -a. p.runOrder(ctx, identifiers) case "preauthz": p.runPreauthz(ctx, identifiers) - case "preauthz02": - p.runPreauthzLegacy(ctx, identifiers) default: log.Fatalf("unknown flow: %q", *flow) } @@ -276,50 +273,6 @@ func (p *prober) runPreauthz(ctx context.Context, identifiers []acme.AuthzID) { } } -func (p *prober) runPreauthzLegacy(ctx context.Context, identifiers []acme.AuthzID) { - var zurls []string - for _, id := range identifiers { - z, err := authorize(ctx, p.client, id) - if err != nil { - log.Fatalf("AuthorizeID(%+v): %v", id, err) - } - if z.Status == acme.StatusValid { - log.Printf("authz %s is valid; skipping", z.URI) - continue - } - if err := p.fulfill(ctx, z); err != nil { - log.Fatalf("fulfill(%s): %v", z.URI, err) - } - zurls = append(zurls, z.URI) - log.Printf("authorized for %+v", id) - } - - // We should be all set now. - log.Print("all authorizations are done") - csr, certkey := newCSR(identifiers) - der, curl, err := p.client.CreateCert(ctx, csr, 48*time.Hour, true) - if err != nil { - log.Fatalf("CreateCert: %v", err) - } - log.Printf("cert URL: %s", curl) - if err := checkCert(der, identifiers); err != nil { - p.errorf("invalid cert: %v", err) - } - - // Deactivate all authorizations we satisfied earlier. - for _, v := range zurls { - if err := p.client.RevokeAuthorization(ctx, v); err != nil { - p.errorf("RevokAuthorization(%q): %v", v, err) - continue - } - } - // Try revoking the issued cert using its private key. - if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil { - p.errorf("RevokeCert: %v", err) - } - -} - func (p *prober) fulfill(ctx context.Context, z *acme.Authorization) error { var chal *acme.Challenge for i, c := range z.Challenges { diff --git a/acme/jws.go b/acme/jws.go index 8c3ecceca7..174c54c4e3 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -51,6 +51,9 @@ type jsonWebSignature struct { // // See https://tools.ietf.org/html/rfc7515#section-7. func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) { + if key == nil { + return nil, errors.New("nil key") + } alg, sha := jwsHasher(key.Public()) if alg == "" || !sha.Available() { return nil, ErrUnsupportedKey diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go index 07e2f29db2..5883de78e4 100644 --- a/acme/rfc8555_test.go +++ b/acme/rfc8555_test.go @@ -62,7 +62,7 @@ func TestRFC_Discover(t *testing.T) { }`, nonce, reg, order, authz, revoke, keychange, metaTerms, metaWebsite, metaCAA) })) defer ts.Close() - c := Client{DirectoryURL: ts.URL} + c := &Client{DirectoryURL: ts.URL} dir, err := c.Discover(context.Background()) if err != nil { t.Fatal(err) diff --git a/acme/types.go b/acme/types.go index eaae452907..67b825201e 100644 --- a/acme/types.go +++ b/acme/types.go @@ -305,14 +305,6 @@ type Directory struct { ExternalAccountRequired bool } -// rfcCompliant reports whether the ACME server implements RFC 8555. -// Note that some servers may have incomplete RFC implementation -// even if the returned value is true. -// If rfcCompliant reports false, the server most likely implements draft-02. -func (d *Directory) rfcCompliant() bool { - return d.OrderURL != "" -} - // Order represents a client's request for a certificate. // It tracks the request flow progress through to issuance. type Order struct {