diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index 1858184e81..0061c2881c 100644 --- a/acme/autocert/autocert.go +++ b/acme/autocert/autocert.go @@ -170,6 +170,11 @@ type Manager struct { // 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 @@ func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) { 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 4ae408fcbb..ab7504a260 100644 --- a/acme/autocert/autocert_test.go +++ b/acme/autocert/autocert_test.go @@ -394,6 +394,19 @@ func TestGetCertificate(t *testing.T) { } }, }, + { + 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 8c4c6426a1..fa33987913 100644 --- a/acme/autocert/internal/acmetest/ca.go +++ b/acme/autocert/internal/acmetest/ca.go @@ -49,6 +49,7 @@ type CAServer struct { challengeTypes []string url string roots *x509.CertPool + eabRequired bool mu sync.Mutex certCount int // number of issued certs @@ -152,6 +153,15 @@ func (ca *CAServer) Roots() *x509.CertPool { 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 @@ type discovery struct { 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 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { 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 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { 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)