From 4663e185863a1aee50d0486b326769f0bd22eb30 Mon Sep 17 00:00:00 2001 From: Alex Vaghin Date: Mon, 9 Sep 2019 15:54:00 -0400 Subject: [PATCH] acme: update existing methods for RFC 8555 This adds RFC support to the existing methods which, in conjunction with the new order based methods implemented in golang.org/cl/192779, completes a Client capable of obtaining certificates from RFC compliant CAs. Updates golang/go#21081 Change-Id: I3aabc50928d3e4e49ee202eb6695135d5ad86821 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/194379 Reviewed-by: Filippo Valsorda --- acme/acme.go | 166 ++++++++++++++++++++++++++----------------- acme/acme_test.go | 12 ++-- acme/http.go | 10 ++- acme/rfc8555.go | 151 ++++++++++++++++++++++++++++++++++++--- acme/rfc8555_test.go | 86 ++++++++++++++++++++-- acme/types.go | 92 +++++++++++++++++------- 6 files changed, 404 insertions(+), 113 deletions(-) diff --git a/acme/acme.go b/acme/acme.go index 31d07e3..fde2acb 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -5,7 +5,7 @@ // Package acme provides an implementation of the // Automatic Certificate Management Environment (ACME) spec. // The intial implementation was based on ACME draft-02 and -// is now being extended to comply with RFC8555. +// 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. // @@ -60,7 +60,10 @@ var idPeACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} const ( maxChainLen = 5 // max depth and breadth of a certificate chain - maxCertSize = 1 << 20 // max size of a certificate, in bytes + maxCertSize = 1 << 20 // max size of a certificate, in DER bytes + // Used for decoding certs from application/pem-certificate-chain response, + // the default when in RFC mode. + maxCertChainSize = maxCertSize * maxChainLen // Max number of collected nonces kept in memory. // Expect usual peak of 1 or 2. @@ -139,8 +142,7 @@ type Client struct { func (c *Client) accountKID(ctx context.Context) keyID { c.cacheMu.Lock() defer c.cacheMu.Unlock() - if c.dir.OrderURL == "" { - // Assume legacy CA. + if !c.dir.rfcCompliant() { return noKeyID } if c.kid != noKeyID { @@ -233,6 +235,9 @@ func (c *Client) directoryURL() string { } // 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. +// // 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. @@ -284,12 +289,22 @@ func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, // It retries the request until the certificate is successfully retrieved, // context is cancelled by the caller or an error response is received. // -// The returned value will also contain the CA (issuer) certificate if the bundle argument is true. +// If the bundle argument is true, the returned value also contains the CA (issuer) +// certificate chain. // // FetchCert 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 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 { return nil, err @@ -304,10 +319,15 @@ 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 { - if _, err := c.Discover(ctx); err != nil { + 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"` @@ -317,7 +337,7 @@ func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, Cert: base64.RawURLEncoding.EncodeToString(cert), Reason: int(reason), } - res, err := c.post(ctx, key, c.dir.RevokeURL, body, wantStatus(http.StatusOK)) + res, err := c.post(ctx, key, dir.RevokeURL, body, wantStatus(http.StatusOK)) if err != nil { return err } @@ -337,7 +357,7 @@ func AcceptTOS(tosURL string) bool { return true } // Register calls prompt with a TOS URL provided by the CA. Prompt should report // whether the caller agrees to the terms. To always accept the terms, the caller can use AcceptTOS. // -// When interfacing with RFC compliant CA, non-RFC8555 compliant fields of acct are ignored +// When interfacing with an RFC-compliant CA, non-RFC 8555 fields of acct are ignored // and prompt is called if Directory's Terms field is non-zero. // Also see Error's Instance field for when a CA requires already registered accounts to agree // to an updated Terms of Service. @@ -346,9 +366,7 @@ func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL if err != nil { return nil, err } - - // RFC8555 compliant account registration. - if dir.OrderURL != "" { + if dir.rfcCompliant() { return c.registerRFC(ctx, acct, prompt) } @@ -370,16 +388,14 @@ func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL // GetReg retrieves an existing account associated with c.Key. // -// The url argument is an Account URI used with pre-RFC8555 CAs. -// It is ignored when interfacing with an RFC compliant CA. +// The url argument is an Account URI used with pre-RFC 8555 CAs. +// It is ignored when interfacing with an RFC-compliant CA. func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) { dir, err := c.Discover(ctx) if err != nil { return nil, err } - - // Assume RFC8555 compliant CA. - if dir.OrderURL != "" { + if dir.rfcCompliant() { return c.getRegRFC(ctx) } @@ -395,16 +411,14 @@ func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) { // 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 +// When interfacing with RFC-compliant CAs, a.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 } - - // Assume RFC8555 compliant CA. - if dir.OrderURL != "" { + if dir.rfcCompliant() { return c.updateRegRFC(ctx, acct) } @@ -418,13 +432,21 @@ func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error) return a, nil } -// Authorize performs the initial step in an authorization flow. +// Authorize performs the initial step in the pre-authorization flow, +// as opposed to order-based flow. // The caller will then need to choose from and perform a set of returned // challenges using c.Accept in order to successfully complete authorization. // +// Once complete, the caller can use AuthorizeOrder which the CA +// should provision with the already satisfied authorization. +// For pre-RFC CAs, the caller can proceed directly to requesting a certificate +// using CreateCert method. +// // If an authorization has been previously granted, the CA may return -// a valid authorization (Authorization.Status is StatusValid). If so, the caller -// need not fulfill any challenge and can proceed to requesting a certificate. +// a valid authorization which has its Status field set to StatusValid. +// +// More about pre-authorization can be found at +// https://tools.ietf.org/html/rfc8555#section-7.4.1. func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) { return c.authorize(ctx, "dns", domain) } @@ -476,7 +498,17 @@ 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) { - res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + dir, err := c.Discover(ctx) + if 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)) + } if err != nil { return nil, err } @@ -493,8 +525,8 @@ func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorizati // The url argument is an Authorization.URI value. // // If successful, the caller will be required to obtain a new authorization -// using the Authorize method before being able to request a new certificate -// for the domain associated with the authorization. +// using the Authorize or AuthorizeOrder methods before being able to request +// a new certificate for the domain associated with the authorization. // // It does not revoke existing certificates. func (c *Client) RevokeAuthorization(ctx context.Context, url string) error { @@ -528,8 +560,18 @@ 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 { + return nil, err + } + getfn := c.postAsGet + if !dir.rfcCompliant() { + getfn = c.get + } + for { - res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) if err != nil { return nil, err } @@ -572,10 +614,21 @@ 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) { - res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + // Required for c.accountKID() when in RFC mode. + dir, err := c.Discover(ctx) if err != nil { return nil, err } + + getfn := c.postAsGet + if !dir.rfcCompliant() { + getfn = c.get + } + res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + if err != nil { + return nil, err + } + defer res.Body.Close() v := wireChallenge{URI: url} if err := json.NewDecoder(res.Body).Decode(&v); err != nil { @@ -590,23 +643,26 @@ 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. - if _, err := c.Discover(ctx); err != nil { - return nil, err - } - - auth, err := keyAuth(c.Key.Public(), chal.Token) + dir, err := c.Discover(ctx) 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, + 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( http.StatusOK, // according to the spec @@ -658,21 +714,8 @@ func (c *Client) HTTP01ChallengePath(token string) string { } // TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response. -// Servers can present the certificate to validate the challenge and prove control -// over a domain name. // -// The implementation is incomplete in that the returned value is a single certificate, -// computed only for Z0 of the key authorization. ACME CAs are expected to update -// their implementations to use the newer version, TLS-SNI-02. -// For more details on TLS-SNI-01 see https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3. -// -// The token argument is a Challenge.Token value. -// If a WithKey option is provided, its private part signs the returned cert, -// and the public part is used to specify the signee. -// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve. -// -// The returned certificate is valid for the next 24 hours and must be presented only when -// the server name of the TLS ClientHello matches exactly the returned name value. +// Deprecated: This challenge type is unused in both draft-02 and RFC versions of 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 { @@ -689,17 +732,8 @@ func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tl } // TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response. -// Servers can present the certificate to validate the challenge and prove control -// over a domain name. For more details on TLS-SNI-02 see -// https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3. // -// The token argument is a Challenge.Token value. -// If a WithKey option is provided, its private part signs the returned cert, -// and the public part is used to specify the signee. -// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve. -// -// The returned certificate is valid for the next 24 hours and must be presented only when -// the server name in the TLS ClientHello matches exactly the returned name value. +// Deprecated: This challenge type is unused in both draft-02 and RFC versions of 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[:]) @@ -766,7 +800,7 @@ func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) return tlsChallengeCert([]string{domain}, newOpt) } -// doReg sends all types of registration requests. +// 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. // diff --git a/acme/acme_test.go b/acme/acme_test.go index e17f375..0417a7e 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -484,7 +484,7 @@ func TestGetAuthorization(t *testing.T) { })) defer ts.Close() - cl := Client{Key: testKeyEC} + cl := Client{Key: testKeyEC, DirectoryURL: ts.URL} auth, err := cl.GetAuthorization(context.Background(), ts.URL) if err != nil { t.Fatal(err) @@ -614,7 +614,7 @@ func runWaitAuthorization(ctx context.Context, t *testing.T, h http.HandlerFunc) } ch := make(chan res, 1) go func() { - var client Client + var client = Client{DirectoryURL: ts.URL} a, err := client.WaitAuthorization(ctx, ts.URL) ch <- res{a, err} }() @@ -684,7 +684,7 @@ func TestPollChallenge(t *testing.T) { })) defer ts.Close() - cl := Client{Key: testKeyEC} + cl := Client{Key: testKeyEC, DirectoryURL: ts.URL} chall, err := cl.GetChallenge(context.Background(), ts.URL) if err != nil { t.Fatal(err) @@ -865,7 +865,8 @@ func TestFetchCert(t *testing.T) { w.Write([]byte{count}) })) defer ts.Close() - res, err := (&Client{}).FetchCert(context.Background(), ts.URL, true) + cl := &Client{dir: &Directory{}} // skip discovery + res, err := cl.FetchCert(context.Background(), ts.URL, true) if err != nil { t.Fatalf("FetchCert: %v", err) } @@ -887,7 +888,8 @@ func TestFetchCertRetry(t *testing.T) { w.Write([]byte{1}) })) defer ts.Close() - res, err := (&Client{}).FetchCert(context.Background(), ts.URL, false) + cl := &Client{dir: &Directory{}} // skip discovery + res, err := cl.FetchCert(context.Background(), ts.URL, false) if err != nil { t.Fatalf("FetchCert: %v", err) } diff --git a/acme/http.go b/acme/http.go index b145292..c51943e 100644 --- a/acme/http.go +++ b/acme/http.go @@ -155,6 +155,14 @@ func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Respons } } +// postAsGet is POST-as-GET, a replacement for GET in RFC8555 +// as described in https://tools.ietf.org/html/rfc8555#section-6.3. +// It makes a POST request in KID form with zero JWS payload. +// See nopayload doc comments in jws.go. +func (c *Client) postAsGet(ctx context.Context, url string, ok resOkay) (*http.Response, error) { + return c.post(ctx, nil, url, noPayload, ok) +} + // post issues a signed POST request in JWS format using the provided key // to the specified URL. If key is nil, c.Key is used instead. // It returns a non-error value only when ok reports true. @@ -200,7 +208,7 @@ func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body i // If key argument is nil and c.accountKID returns a non-zero keyID, // the request is sent in KID form. Otherwise, JWK form is used. // -// In practice, when interfacing with RFC compliant CAs most requests are sent in KID form +// In practice, when interfacing with RFC-compliant CAs most requests are sent in KID form // and JWK is used only when KID is unavailable: new account endpoint and certificate // revocation requests authenticated by a cert key. // See jwsEncodeJSON for other details. diff --git a/acme/rfc8555.go b/acme/rfc8555.go index 7517b05..dfb57a6 100644 --- a/acme/rfc8555.go +++ b/acme/rfc8555.go @@ -6,8 +6,14 @@ package acme import ( "context" + "crypto" + "encoding/base64" "encoding/json" + "encoding/pem" + "errors" "fmt" + "io" + "io/ioutil" "net/http" "time" ) @@ -16,7 +22,7 @@ import ( // A deactivated account can no longer request certificate issuance or access // resources related to the account, such as orders or authorizations. // -// It works only with RFC8555 compliant CAs. +// It only works with CAs implementing RFC 8555. func (c *Client) DeactivateReg(ctx context.Context) error { url := string(c.accountKID(ctx)) if url == "" { @@ -31,7 +37,7 @@ func (c *Client) DeactivateReg(ctx context.Context) error { return nil } -// registerRFC is quivalent to c.Register but for RFC-compliant CAs. +// registerRFC is quivalent to c.Register but for CAs implementing RFC 8555. // It expects c.Discover to have already been called. // TODO: Implement externalAccountBinding. func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) { @@ -69,7 +75,7 @@ func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tos return a, nil } -// updateGegRFC is equivalent to c.UpdateReg but for RFC-compliant CAs. +// updateGegRFC is equivalent to c.UpdateReg but for CAs implementing RFC 8555. // It expects c.Discover to have already been called. func (c *Client) updateRegRFC(ctx context.Context, a *Account) (*Account, error) { url := string(c.accountKID(ctx)) @@ -89,7 +95,7 @@ func (c *Client) updateRegRFC(ctx context.Context, a *Account) (*Account, error) return responseAccount(res) } -// getGegRFC is equivalent to c.GetReg but for RFC-compliant CAs. +// getGegRFC is equivalent to c.GetReg but for CAs implementing RFC 8555. // It expects c.Discover to have already been called. func (c *Client) getRegRFC(ctx context.Context) (*Account, error) { req := json.RawMessage(`{"onlyReturnExisting": true}`) @@ -124,10 +130,12 @@ func responseAccount(res *http.Response) (*Account, error) { // AuthorizeOrder initiates the order-based application for certificate issuance, // as opposed to pre-authorization in Authorize. +// It is only supported by CAs implementing RFC 8555. // -// The caller then needs to fetch each required authorization with GetAuthorization -// and fulfill a challenge using Accept. Once all authorizations are satisfied, -// the caller will typically want to poll order status using WaitOrder until it's in StatusReady state. +// The caller then needs to fetch each authorization with GetAuthorization, +// identify those with StatusPending status and fulfill a challenge using Accept. +// Once all authorizations are satisfied, the caller will typically want to poll +// order status using WaitOrder until it's in StatusReady state. // To finalize the order and obtain a certificate, the caller submits a CSR with CreateOrderCert. func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderOption) (*Order, error) { dir, err := c.Discover(ctx) @@ -176,7 +184,7 @@ func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) { return nil, err } - res, err := c.post(ctx, nil, url, noPayload, wantStatus(http.StatusOK)) + res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK)) if err != nil { return nil, err } @@ -190,13 +198,13 @@ func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) { // // It returns a non-nil Order only if its Status is StatusReady or StatusValid. // In all other cases WaitOrder returns an error. -// If the Status is StatusInvalid, the returned error is of type *WaitOrderError. +// If the Status is StatusInvalid, the returned error is of type *OrderError. func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) { if _, err := c.Discover(ctx); err != nil { return nil, err } for { - res, err := c.post(ctx, nil, url, noPayload, wantStatus(http.StatusOK)) + res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK)) if err != nil { return nil, err } @@ -206,7 +214,7 @@ func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) { case err != nil: // Skip and retry. case o.Status == StatusInvalid: - return nil, &WaitOrderError{OrderURL: o.URI, Status: o.Status} + return nil, &OrderError{OrderURL: o.URI, Status: o.Status} case o.Status == StatusReady || o.Status == StatusValid: return o, nil } @@ -261,3 +269,124 @@ func responseOrder(res *http.Response) (*Order, error) { } return o, nil } + +// CreateOrderCert submits the CSR (Certificate Signing Request) to a CA at the specified URL. +// The URL is the FinalizeURL field of an Order created with AuthorizeOrder. +// +// If the bundle argument is true, the returned value also contain the CA (issuer) +// certificate chain. Otherwise, only a leaf certificate is returned. +// The returned URL can be used to re-fetch the certificate using FetchCert. +// +// This method is only supported by CAs implementing RFC 8555. See CreateCert for pre-RFC CAs. +// +// CreateOrderCert returns an error if the CA's response is unreasonably large. +// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features. +func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) { + if _, err := c.Discover(ctx); err != nil { // required by c.accountKID + return nil, "", err + } + + // RFC describes this as "finalize order" request. + req := struct { + CSR string `json:"csr"` + }{ + CSR: base64.RawURLEncoding.EncodeToString(csr), + } + res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK)) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + o, err := responseOrder(res) + if err != nil { + return nil, "", err + } + + // Wait for CA to issue the cert if they haven't. + if o.Status != StatusValid { + o, err = c.WaitOrder(ctx, o.URI) + } + if err != nil { + return nil, "", err + } + // The only acceptable status post finalize and WaitOrder is "valid". + if o.Status != StatusValid { + return nil, "", &OrderError{OrderURL: o.URI, Status: o.Status} + } + crt, err := c.fetchCertRFC(ctx, o.CertURL, bundle) + return crt, o.CertURL, err +} + +// fetchCertRFC downloads issued certificate from the given URL. +// It expects the CA to respond with PEM-encoded certificate chain. +// +// The URL argument is the CertURL field of Order. +func (c *Client) fetchCertRFC(ctx context.Context, url string, bundle bool) ([][]byte, error) { + res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK)) + if err != nil { + return nil, err + } + defer res.Body.Close() + + // Get all the bytes up to a sane maximum. + // Account very roughly for base64 overhead. + const max = maxCertChainSize + maxCertChainSize/33 + b, err := ioutil.ReadAll(io.LimitReader(res.Body, max+1)) + if err != nil { + return nil, fmt.Errorf("acme: fetch cert response stream: %v", err) + } + if len(b) > max { + return nil, errors.New("acme: certificate chain is too big") + } + + // Decode PEM chain. + var chain [][]byte + for { + var p *pem.Block + p, b = pem.Decode(b) + if p == nil { + break + } + if p.Type != "CERTIFICATE" { + return nil, fmt.Errorf("acme: invalid PEM cert type %q", p.Type) + } + + chain = append(chain, p.Bytes) + if !bundle { + return chain, nil + } + if len(chain) > maxChainLen { + return nil, errors.New("acme: certificate chain is too long") + } + } + if len(chain) == 0 { + return nil, errors.New("acme: certificate chain is empty") + } + return chain, nil +} + +// sends a cert revocation request in either JWK form when key is non-nil or KID form otherwise. +func (c *Client) revokeCertRFC(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error { + req := &struct { + Cert string `json:"certificate"` + Reason int `json:"reason"` + }{ + Cert: base64.RawURLEncoding.EncodeToString(cert), + Reason: int(reason), + } + res, err := c.post(ctx, key, c.dir.RevokeURL, req, wantStatus(http.StatusOK)) + if err != nil { + if isAlreadyRevoked(err) { + // Assume it is not an error to revoke an already revoked cert. + return nil + } + return err + } + defer res.Body.Close() + return nil +} + +func isAlreadyRevoked(err error) bool { + e, ok := err.(*Error) + return ok && e.ProblemType == "urn:ietf:params:acme:error:alreadyRevoked" +} diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go index a109c8d..7e5e446 100644 --- a/acme/rfc8555_test.go +++ b/acme/rfc8555_test.go @@ -7,9 +7,14 @@ package acme import ( "bytes" "context" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" + "encoding/pem" "fmt" "io/ioutil" + "math/big" "net/http" "net/http/httptest" "reflect" @@ -569,12 +574,12 @@ func TestRFC_GetOrder(t *testing.T) { func TestRFC_WaitOrder(t *testing.T) { for _, st := range []string{StatusReady, StatusValid} { t.Run(st, func(t *testing.T) { - testWaitOrder(t, st) + testWaitOrderStatus(t, st) }) } } -func testWaitOrder(t *testing.T, okStatus string) { +func testWaitOrderStatus(t *testing.T, okStatus string) { s := newACMEServer() s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", s.url("/accounts/1")) @@ -651,9 +656,9 @@ func TestRFC_WaitOrderError(t *testing.T) { if err == nil { t.Fatal("WaitOrder returned nil error") } - e, ok := err.(*WaitOrderError) + e, ok := err.(*OrderError) if !ok { - t.Fatalf("err = %v (%T); want WaitOrderError", err, err) + t.Fatalf("err = %v (%T); want OrderError", err, err) } if e.OrderURL != s.url("/orders/1") { t.Errorf("e.OrderURL = %q; want %q", e.OrderURL, s.url("/orders/1")) @@ -663,3 +668,76 @@ func TestRFC_WaitOrderError(t *testing.T) { } } } + +func TestRFC_CreateOrderCert(t *testing.T) { + q := &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "example.org"}, + } + csr, err := x509.CreateCertificateRequest(rand.Reader, q, testKeyEC) + if err != nil { + t.Fatal(err) + } + + tmpl := &x509.Certificate{SerialNumber: big.NewInt(1)} + leaf, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &testKeyEC.PublicKey, testKeyEC) + if err != nil { + t.Fatal(err) + } + + s := newACMEServer() + s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", s.url("/accounts/1")) + w.Write([]byte(`{"status": "valid"}`)) + }) + var count int + s.handle("/pleaseissue", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", s.url("/pleaseissue")) + st := StatusProcessing + if count > 0 { + st = StatusValid + } + fmt.Fprintf(w, `{"status":%q, "certificate":%q}`, st, s.url("/crt")) + count++ + }) + s.handle("/crt", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/pem-certificate-chain") + pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: leaf}) + }) + s.start() + defer s.close() + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")} + cert, curl, err := cl.CreateOrderCert(ctx, s.url("/pleaseissue"), csr, true) + if err != nil { + t.Fatalf("CreateOrderCert: %v", err) + } + if _, err := x509.ParseCertificate(cert[0]); err != nil { + t.Errorf("ParseCertificate: %v", err) + } + if !reflect.DeepEqual(cert[0], leaf) { + t.Errorf("cert and leaf bytes don't match") + } + if u := s.url("/crt"); curl != u { + t.Errorf("curl = %q; want %q", curl, u) + } +} + +func TestRFC_AlreadyRevokedCert(t *testing.T) { + s := newACMEServer() + s.handle("/acme/revoke-cert", func(w http.ResponseWriter, r *http.Request) { + s.error(w, &wireError{ + Status: http.StatusBadRequest, + Type: "urn:ietf:params:acme:error:alreadyRevoked", + }) + }) + s.start() + defer s.close() + + cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")} + err := cl.RevokeCert(context.Background(), testKeyEC, []byte{0}, CRLReasonUnspecified) + if err != nil { + t.Fatalf("RevokeCert: %v", err) + } +} diff --git a/acme/types.go b/acme/types.go index c654fff..9c59097 100644 --- a/acme/types.go +++ b/acme/types.go @@ -18,10 +18,11 @@ import ( // See https://tools.ietf.org/html/rfc8555#section-7.1.6 for details. const ( StatusDeactivated = "deactivated" + StatusExpired = "expired" StatusInvalid = "invalid" - StatusReady = "ready" StatusPending = "pending" StatusProcessing = "processing" + StatusReady = "ready" StatusRevoked = "revoked" StatusUnknown = "unknown" StatusValid = "valid" @@ -104,16 +105,19 @@ func (a *AuthorizationError) Error() string { return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; ")) } -// WaitOrderError is returned from Client's WaitOrder. +// OrderError is returned from Client's order related methods. // It indicates the order is unusable and the clients should start over with // AuthorizeOrder. -type WaitOrderError struct { +// +// The clients can still fetch the order object from CA using GetOrder +// to inspect its state. +type OrderError struct { OrderURL string Status string } -func (we *WaitOrderError) Error() string { - return fmt.Sprintf("acme: wait order %s: status %s", we.OrderURL, we.Status) +func (oe *OrderError) Error() string { + return fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status) } // RateLimit reports whether err represents a rate limit error and @@ -138,11 +142,11 @@ func RateLimit(err error) (time.Duration, bool) { } // Account is a user account. It is associated with a private key. -// Non-RFC8555 fields are empty when interfacing with a compliant CA. +// Non-RFC 8555 fields are empty when interfacing with a compliant CA. type Account struct { // URI is the account unique ID, which is also a URL used to retrieve // account data from the CA. - // When interfacing with RFC8555-compliant CAs, URI is the "kid" field + // When interfacing with RFC 8555-compliant CAs, URI is the "kid" field // value in JWS signed requests. URI string @@ -163,32 +167,32 @@ type Account struct { // A value not matching CurrentTerms indicates that the user hasn't agreed // to the actual Terms of Service of the CA. // - // It is non-RFC8555 compliant. Package users can store the ToS they agree to + // It is non-RFC 8555 compliant. Package users can store the ToS they agree to // during Client's Register call in the prompt callback function. AgreedTerms string // Actual terms of a CA. // - // It is non-RFC8555 compliant. Use Directory's Terms field. + // It is non-RFC 8555 compliant. Use Directory's Terms field. // When a CA updates their terms and requires an account agreement, // a URL at which instructions to do so is available in Error's Instance field. CurrentTerms string // Authz is the authorization URL used to initiate a new authz flow. // - // It is non-RFC8555 compliant. Use Directory's AuthzURL or OrderURL. + // It is non-RFC 8555 compliant. Use Directory's AuthzURL or OrderURL. Authz string // Authorizations is a URI from which a list of authorizations // granted to this account can be fetched via a GET request. // - // It is non-RFC8555 compliant and is obsoleted by OrdersURL. + // It is non-RFC 8555 compliant and is obsoleted by OrdersURL. Authorizations string // Certificates is a URI from which a list of certificates // issued for this account can be fetched via a GET request. // - // It is non-RFC8555 compliant and is obsoleted by OrdersURL. + // It is non-RFC 8555 compliant and is obsoleted by OrdersURL. Certificates string } @@ -199,11 +203,11 @@ type Directory struct { NonceURL string // RegURL is an account endpoint URL, allowing for creating new accounts. - // Pre-RFC8555 CAs also allow modifying existing accounts at this URL. + // Pre-RFC 8555 CAs also allow modifying existing accounts at this URL. RegURL string // OrderURL is used to initiate the certificate issuance flow - // as described in RFC8555. + // as described in RFC 8555. OrderURL string // AuthzURL is used to initiate identifier pre-authorization flow. @@ -211,7 +215,7 @@ type Directory struct { AuthzURL string // CertURL is a new certificate issuance endpoint URL. - // It is non-RFC8555 compliant and is obsoleted by OrderURL. + // It is non-RFC 8555 compliant and is obsoleted by OrderURL. CertURL string // RevokeURL is used to initiate a certificate revocation flow. @@ -237,6 +241,14 @@ 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 { @@ -323,15 +335,26 @@ type Authorization struct { // URI uniquely identifies a authorization. URI string - // Status identifies the status of an authorization. + // Status is the current status of an authorization. + // Possible values are StatusPending, StatusValid, StatusInvalid, StatusDeactivated, + // StatusExpired and StatusRevoked. Status string // Identifier is what the account is authorized to represent. Identifier AuthzID + // The timestamp after which the CA considers the authorization invalid. + Expires time.Time + + // Wildcard is true for authorizations of a wildcard domain name. + Wildcard bool + // Challenges that the client needs to fulfill in order to prove possession // of the identifier (for pending authorizations). - // For final authorizations, the challenges that were used. + // For valid authorizations, the challenge that was validated. + // For invalid authorizations, the challenge that was attempted and failed. + // + // RFC 8555 compatible CAs require users to fuflfill only one of the challenges. Challenges []*Challenge // A collection of sets of challenges, each of which would be sufficient @@ -339,6 +362,8 @@ type Authorization struct { // Clients must complete a set of challenges that covers at least one set. // Challenges are identified by their indices in the challenges array. // If this field is empty, the client needs to complete all challenges. + // + // This field is unused in RFC 8555. Combinations [][]int } @@ -376,10 +401,12 @@ type wireAuthzID struct { // wireAuthz is ACME JSON representation of Authorization objects. type wireAuthz struct { + Identifier wireAuthzID Status string + Expires time.Time + Wildcard bool Challenges []wireChallenge Combinations [][]int - Identifier wireAuthzID } func (z *wireAuthz) authorization(uri string) *Authorization { @@ -387,8 +414,10 @@ func (z *wireAuthz) authorization(uri string) *Authorization { URI: uri, Status: z.Status, Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value}, - Combinations: z.Combinations, // shallow copy + Expires: z.Expires, + Wildcard: z.Wildcard, Challenges: make([]*Challenge, len(z.Challenges)), + Combinations: z.Combinations, // shallow copy } for i, v := range z.Challenges { a.Challenges[i] = v.challenge() @@ -413,7 +442,7 @@ func (z *wireAuthz) error(uri string) *AuthorizationError { // Its Error field may be non-nil if the challenge is part of an Authorization // with StatusInvalid. type Challenge struct { - // Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01". + // Type is the challenge type, e.g. "http-01", "tls-alpn-01", "dns-01". Type string // URI is where a challenge response can be posted to. @@ -423,8 +452,14 @@ type Challenge struct { Token string // Status identifies the status of this challenge. + // In RFC 8555, possible values are StatusPending, StatusProcessing, StatusValid, + // and StatusInvalid. Status string + // Validated is the time at which the CA validated this challenge. + // Always zero value in pre-RFC 8555. + Validated time.Time + // Error indicates the reason for an authorization failure // when this challenge was used. // The type of a non-nil value is *Error. @@ -433,20 +468,25 @@ type Challenge struct { // wireChallenge is ACME JSON challenge representation. type wireChallenge struct { - URI string `json:"uri"` - Type string - Token string - Status string - Error *wireError + URL string `json:"url"` // RFC + URI string `json:"uri"` // pre-RFC + Type string + Token string + Status string + Validated time.Time + Error *wireError } func (c *wireChallenge) challenge() *Challenge { v := &Challenge{ - URI: c.URI, + URI: c.URL, Type: c.Type, Token: c.Token, Status: c.Status, } + if v.URI == "" { + v.URI = c.URI // c.URL was empty; use legacy + } if v.Status == "" { v.Status = StatusPending }