acme/autocert: support External Account Binding (EAB) tokens

Support External Account Binding (EAB) tokens to the Manager as defined
in RFC 8555, Section 7.3.4. If the ExternalAccountBinding field is set
on Manager, pass it into the acme Account during registration.

Fixes golang/go#48809

Change-Id: I64c38b05ab577acbde9f526638cc8104d15ff055
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/354189
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Trust: Brad Fitzpatrick <bradfitz@golang.org>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Run-TryBot: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go
index 1858184..0061c28 100644
--- a/acme/autocert/autocert.go
+++ b/acme/autocert/autocert.go
@@ -170,6 +170,11 @@
 	// in the template's ExtraExtensions field as is.
 	ExtraExtensions []pkix.Extension
 
+	// ExternalAccountBinding optionally represents an arbitrary binding to an
+	// account of the CA to which the ACME server is tied.
+	// See RFC 8555, Section 7.3.4 for more details.
+	ExternalAccountBinding *acme.ExternalAccountBinding
+
 	clientMu sync.Mutex
 	client   *acme.Client // initialized by acmeClient method
 
@@ -996,7 +1001,7 @@
 	if m.Email != "" {
 		contact = []string{"mailto:" + m.Email}
 	}
-	a := &acme.Account{Contact: contact}
+	a := &acme.Account{Contact: contact, ExternalAccountBinding: m.ExternalAccountBinding}
 	_, err := client.Register(ctx, a, m.Prompt)
 	if err == nil || isAccountAlreadyExist(err) {
 		m.client = client
diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go
index 4ae408f..ab7504a 100644
--- a/acme/autocert/autocert_test.go
+++ b/acme/autocert/autocert_test.go
@@ -394,6 +394,19 @@
 				}
 			},
 		},
+		{
+			name:   "provideExternalAuth",
+			hello:  clientHelloInfo("example.org", algECDSA),
+			domain: "example.org",
+			prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
+				s.ExternalAccountRequired()
+
+				man.ExternalAccountBinding = &acme.ExternalAccountBinding{
+					KID: "test-key",
+					Key: make([]byte, 32),
+				}
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/acme/autocert/internal/acmetest/ca.go b/acme/autocert/internal/acmetest/ca.go
index 8c4c642..fa33987 100644
--- a/acme/autocert/internal/acmetest/ca.go
+++ b/acme/autocert/internal/acmetest/ca.go
@@ -49,6 +49,7 @@
 	challengeTypes []string
 	url            string
 	roots          *x509.CertPool
+	eabRequired    bool
 
 	mu             sync.Mutex
 	certCount      int                           // number of issued certs
@@ -152,6 +153,15 @@
 	return ca.roots
 }
 
+// ExternalAccountRequired makes an EAB JWS required for account registration.
+func (ca *CAServer) ExternalAccountRequired() *CAServer {
+	if ca.url != "" {
+		panic("ExternalAccountRequired must be called before Start")
+	}
+	ca.eabRequired = true
+	return ca
+}
+
 // Start starts serving requests. The server address becomes available in the
 // URL field.
 func (ca *CAServer) Start() *CAServer {
@@ -224,6 +234,12 @@
 	NewAccount string `json:"newAccount"`
 	NewOrder   string `json:"newOrder"`
 	NewAuthz   string `json:"newAuthz"`
+
+	Meta discoveryMeta `json:"meta,omitempty"`
+}
+
+type discoveryMeta struct {
+	ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
 }
 
 type challenge struct {
@@ -264,6 +280,9 @@
 			NewNonce:   ca.serverURL("/new-nonce"),
 			NewAccount: ca.serverURL("/new-account"),
 			NewOrder:   ca.serverURL("/new-order"),
+			Meta: discoveryMeta{
+				ExternalAccountRequired: ca.eabRequired,
+			},
 		}
 		if err := json.NewEncoder(w).Encode(resp); err != nil {
 			panic(fmt.Sprintf("discovery response: %v", err))
@@ -283,6 +302,21 @@
 			return
 		}
 		ca.acctRegistered = true
+
+		var req struct {
+			ExternalAccountBinding json.RawMessage
+		}
+
+		if err := decodePayload(&req, r.Body); err != nil {
+			ca.httpErrorf(w, http.StatusBadRequest, err.Error())
+			return
+		}
+
+		if ca.eabRequired && len(req.ExternalAccountBinding) == 0 {
+			ca.httpErrorf(w, http.StatusBadRequest, "registration failed: no JWS for EAB")
+			return
+		}
+
 		// TODO: Check the user account key against a ca.accountKeys?
 		w.Header().Set("Location", ca.serverURL("/accounts/1"))
 		w.WriteHeader(http.StatusCreated)