From 2afe7c4bde1bf8be2ca50c448b6b8142970faf3e Mon Sep 17 00:00:00 2001 From: x1ddos Date: Tue, 20 Aug 2019 17:44:11 +0200 Subject: [PATCH] acme: add support for RFC8555 compliant discovery This CL is part of many to extend existing acme package functionality to support RFC8555 without breaking existing users of both this client package and CA implementations which haven't caught up to the RFC spec. Updates golang/go#21081 Change-Id: I20eb339ede019930c3482286cd13a3ba6f2b3cd6 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/182937 Run-TryBot: Alex Vaghin TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- acme/acme.go | 61 ++++++++++++++++++++++--------- acme/rfc8555_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++ acme/types.go | 19 +++++++++- 3 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 acme/rfc8555_test.go diff --git a/acme/acme.go b/acme/acme.go index fa365b7..3cf7486 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -4,7 +4,10 @@ // Package acme provides an implementation of the // Automatic Certificate Management Environment (ACME) spec. -// See https://tools.ietf.org/html/draft-ietf-acme-acme-02 for details. +// The intial implementation was based on ACME draft-02 and +// is now being extended to comply with RFC8555. +// See https://tools.ietf.org/html/draft-ietf-acme-acme-02 +// and https://tools.ietf.org/html/rfc8555 for details. // // Most common scenarios will want to use autocert subdirectory instead, // which provides automatic access to certificates from Let's Encrypt @@ -143,27 +146,53 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) { c.addNonce(res.Header) var v struct { - Reg string `json:"new-reg"` - Authz string `json:"new-authz"` - Cert string `json:"new-cert"` - Revoke string `json:"revoke-cert"` - Meta struct { - Terms string `json:"terms-of-service"` - Website string `json:"website"` - CAA []string `json:"caa-identities"` + 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"` } } 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 + } + // 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.Website, - CAA: v.Meta.CAA, + 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, } return *c.dir, nil } diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go new file mode 100644 index 0000000..bfb5e53 --- /dev/null +++ b/acme/rfc8555_test.go @@ -0,0 +1,86 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package acme + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +// While contents of this file is pertinent only to RFC8555, +// it is complementary to the tests in the other _test.go files +// many of which are valid for both pre- and RFC8555. +// This will make it easier to clean up the tests once non-RFC compliant +// code is removed. + +func TestRFC_Discover(t *testing.T) { + const ( + nonce = "https://example.com/acme/new-nonce" + reg = "https://example.com/acme/new-acct" + order = "https://example.com/acme/new-order" + authz = "https://example.com/acme/new-authz" + revoke = "https://example.com/acme/revoke-cert" + keychange = "https://example.com/acme/key-change" + metaTerms = "https://example.com/acme/terms/2017-5-30" + metaWebsite = "https://www.example.com/" + metaCAA = "example.com" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "newNonce": %q, + "newAccount": %q, + "newOrder": %q, + "newAuthz": %q, + "revokeCert": %q, + "keyChange": %q, + "meta": { + "termsOfService": %q, + "website": %q, + "caaIdentities": [%q], + "externalAccountRequired": true + } + }`, nonce, reg, order, authz, revoke, keychange, metaTerms, metaWebsite, metaCAA) + })) + defer ts.Close() + c := Client{DirectoryURL: ts.URL} + dir, err := c.Discover(context.Background()) + if err != nil { + t.Fatal(err) + } + if dir.NonceURL != nonce { + t.Errorf("dir.NonceURL = %q; want %q", dir.NonceURL, nonce) + } + if dir.RegURL != reg { + t.Errorf("dir.RegURL = %q; want %q", dir.RegURL, reg) + } + if dir.OrderURL != order { + t.Errorf("dir.OrderURL = %q; want %q", dir.OrderURL, order) + } + if dir.AuthzURL != authz { + t.Errorf("dir.AuthzURL = %q; want %q", dir.AuthzURL, authz) + } + if dir.RevokeURL != revoke { + t.Errorf("dir.RevokeURL = %q; want %q", dir.RevokeURL, revoke) + } + if dir.KeyChangeURL != keychange { + t.Errorf("dir.KeyChangeURL = %q; want %q", dir.KeyChangeURL, keychange) + } + if dir.Terms != metaTerms { + t.Errorf("dir.Terms = %q; want %q", dir.Terms, metaTerms) + } + if dir.Website != metaWebsite { + t.Errorf("dir.Website = %q; want %q", dir.Website, metaWebsite) + } + if len(dir.CAA) == 0 || dir.CAA[0] != metaCAA { + t.Errorf("dir.CAA = %q; want [%q]", dir.CAA, metaCAA) + } + if !dir.ExternalAccountRequired { + t.Error("dir.Meta.ExternalAccountRequired is false") + } +} diff --git a/acme/types.go b/acme/types.go index 54792c0..a411487 100644 --- a/acme/types.go +++ b/acme/types.go @@ -137,20 +137,33 @@ type Account struct { } // Directory is ACME server discovery data. +// See https://tools.ietf.org/html/rfc8555#section-7.1.1 for more details. type Directory struct { + // NonceURL indicates an endpoint where to fetch fresh nonce values from. + NonceURL string + // RegURL is an account endpoint URL, allowing for creating new // and modifying existing accounts. RegURL string - // AuthzURL is used to initiate Identifier Authorization flow. + // OrderURL is used to initiate the certificate issuance flow + // as described in RFC8555. + OrderURL string + + // AuthzURL is used to initiate identifier pre-authorization flow. + // Empty string indicates the flow is unsupported by the CA. AuthzURL string // CertURL is a new certificate issuance endpoint URL. + // It is non-RFC8555 compliant and is obsoleted by OrderURL. CertURL string // RevokeURL is used to initiate a certificate revocation flow. RevokeURL string + // KeyChangeURL allows to perform account key rollover flow. + KeyChangeURL string + // Term is a URI identifying the current terms of service. Terms string @@ -162,6 +175,10 @@ type Directory struct { // recognises as referring to itself for the purposes of CAA record validation // as defined in RFC6844. CAA []string + + // ExternalAccountRequired indicates that the CA requires for all account-related + // requests to include external account binding information. + ExternalAccountRequired bool } // Challenge encodes a returned CA challenge.