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 <ddos@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
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 @@
 	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 @@
 }
 
 // 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 @@
 	// 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.