diff --git a/acme/api/account.go b/acme/api/account.go index 12a3ea47..a917b0e0 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -9,8 +9,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/logging" - - squarejose "gopkg.in/square/go-jose.v2" + "go.step.sm/crypto/jose" ) // ExternalAccountBinding represents the ACME externalAccountBinding JWS @@ -94,12 +93,6 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } - eak, err := h.validateExternalAccountBinding(ctx, &nar) - if err != nil { - api.WriteError(w, err) - return - } - prov, err := acmeProvisionerFromContext(ctx) if err != nil { api.WriteError(w, err) @@ -107,7 +100,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { } httpStatus := http.StatusCreated - acc, err := accountFromContext(r.Context()) + acc, err := accountFromContext(ctx) if err != nil { acmeErr, ok := err.(*acme.Error) if !ok || acmeErr.Status != http.StatusBadRequest { @@ -122,12 +115,19 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { "account does not exist")) return } + jwk, err := jwkFromContext(ctx) if err != nil { api.WriteError(w, err) return } + eak, err := h.validateExternalAccountBinding(ctx, &nar) + if err != nil { + api.WriteError(w, err) + return + } + acc = &acme.Account{ Key: jwk, Contact: nar.Contact, @@ -137,6 +137,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { api.WriteError(w, acme.WrapErrorISE(err, "error creating account")) return } + if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response err := eak.BindTo(acc) if err != nil { @@ -256,26 +257,27 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") } - // TODO: extract the EAB in a similar manner as JWS, JWK, payload, etc? That would probably move a lot/all of - // the logic of this function into the middleware. Should not be too hard, because the middleware does know - // about the handler and thus about its dependencies. eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) if err != nil { return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes") } - eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) + eabJWS, err := jose.ParseJWS(string(eabJSONBytes)) if err != nil { return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") } - // TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration + // TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration? + + keyID, acmeErr := validateEABJWS(ctx, eabJWS) + if acmeErr != nil { + return nil, acmeErr + } - keyID := eabJWS.Signatures[0].Protected.KeyID externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.Name, keyID) if err != nil { if _, ok := err.(*acme.Error); ok { - return nil, err + return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") } return nil, acme.WrapErrorISE(err, "error retrieving external account key") } @@ -294,14 +296,14 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, err } - var payloadJWK *squarejose.JSONWebKey + var payloadJWK *jose.JSONWebKey err = json.Unmarshal(payload, &payloadJWK) if err != nil { return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") } if !keysAreEqual(jwk, payloadJWK) { - return nil, acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use + return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match") } return externalAccountKey, nil @@ -309,7 +311,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc // keysAreEqual performs an equality check on two JWKs by comparing // the (base64 encoding) of the Key IDs. -func keysAreEqual(x, y *squarejose.JSONWebKey) bool { +func keysAreEqual(x, y *jose.JSONWebKey) bool { if x == nil || y == nil { return false } @@ -320,3 +322,62 @@ func keysAreEqual(x, y *squarejose.JSONWebKey) bool { } return digestX == digestY } + +// validateEABJWS verifies the contents of the External Account Binding JWS. +// The protected header of the JWS MUST meet the following criteria: +// o The "alg" field MUST indicate a MAC-based algorithm +// o The "kid" field MUST contain the key identifier provided by the CA +// o The "nonce" field MUST NOT be present +// o The "url" field MUST be set to the same value as the outer JWS +func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) { + + if jws == nil { + return "", acme.NewErrorISE("no JWS provided") + } + + if len(jws.Signatures) != 1 { + return "", acme.NewError(acme.ErrorMalformedType, "JWS must have one signature") + } + + header := jws.Signatures[0].Protected + algorithm := header.Algorithm + keyID := header.KeyID + nonce := header.Nonce + + if !(algorithm == jose.HS256 || algorithm == jose.HS384 || algorithm == jose.HS512) { + return "", acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm '%s'", algorithm) + } + + if keyID == "" { + return "", acme.NewError(acme.ErrorMalformedType, "'kid' field is required") + } + + if nonce != "" { + return "", acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present") + } + + jwsURL, ok := header.ExtraHeaders["url"] + if !ok { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field is required") + } + + outerJWS, err := jwsFromContext(ctx) + if err != nil { + return "", acme.WrapErrorISE(err, "could not retrieve outer JWS from context") + } + + if len(outerJWS.Signatures) != 1 { + return "", acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature") + } + + outerJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders["url"] + if !ok { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS") + } + + if jwsURL != outerJWSURL { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS") + } + + return keyID, nil +} diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 3af97a73..16d43c68 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -11,9 +11,8 @@ import ( "encoding/base64" "encoding/json" "fmt" - "math/big" - "io" + "math/big" "net/http/httptest" "net/url" "testing" @@ -25,7 +24,6 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" - squarejose "gopkg.in/square/go-jose.v2" ) var ( @@ -496,7 +494,8 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, statusCode: 500, @@ -509,7 +508,8 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, nil) return test{ ctx: ctx, @@ -547,6 +547,7 @@ func TestHandler_NewAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, provisionerContextKey, prov) ctx = context.WithValue(ctx, jwkContextKey, jwk) return test{ db: &acme.MockDB{ @@ -561,6 +562,108 @@ func TestHandler_NewAccount(t *testing.T) { err: acme.NewErrorISE("force"), } }, + "fail/acmeProvisionerFromContext": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + scepProvisioner := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + if err := scepProvisioner.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil { + assert.FatalError(t, err) + } + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + return test{ + ctx: ctx, + statusCode: 500, + err: acme.NewError(acme.ErrorServerInternalType, "provisioner in context is not an ACME provisioner"), + } + }, + "fail/db.UpdateExternalAccountKey-error": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + eak := &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + } + return test{ + db: &acme.MockDB{ + MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" + assert.Equals(t, acc.Contact, nar.Contact) + assert.Equals(t, acc.Key, jwk) + return nil + }, + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return eak, nil + }, + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { + return errors.New("force") + }, + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), + ExternalAccountBinding: eab, + }, + ctx: ctx, + statusCode: 500, + err: acme.NewError(acme.ErrorServerInternalType, "error updating external account binding key"), + } + }, "ok/new-account": func(t *testing.T) test { nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -660,7 +763,8 @@ func TestHandler_NewAccount(t *testing.T) { "ok/new-account-with-eab": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) @@ -669,14 +773,29 @@ func TestHandler_NewAccount(t *testing.T) { Contact: []string{"foo", "bar"}, ExternalAccountBinding: eab, } - b, err := json.Marshal(nar) + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { @@ -958,8 +1077,8 @@ func Test_keysAreEqual(t *testing.T) { assert.FatalError(t, err) wrongJWK.Key = struct{}{} type args struct { - x *squarejose.JSONWebKey - y *squarejose.JSONWebKey + x *jose.JSONWebKey + y *jose.JSONWebKey } tests := []struct { name string @@ -1046,16 +1165,38 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { "ok/eab": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) createdAt := time.Now() return test{ db: &acme.MockDB{ @@ -1084,6 +1225,36 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { err: nil, } }, + "fail/acmeProvisionerFromContext": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + scepProvisioner := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + if err := scepProvisioner.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil { + assert.FatalError(t, err) + } + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + return test{ + ctx: ctx, + err: acme.NewError(acme.ErrorServerInternalType, "could not load ACME provisioner from context: provisioner in context is not an ACME provisioner"), + } + }, "fail/parse-eab-jose": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) @@ -1109,19 +1280,88 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { err: acme.NewErrorISE("error parsing externalAccountBinding jws"), } }, - "fail/retrieve-eab-key-db-failure": func(t *testing.T) test { + "fail/validate-eab-jws-no-signatures": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + parsedJWS.Signatures = []jose.Signature{} + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{}, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature"), + } + }, + "fail/retrieve-eab-key-db-failure": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockError: errors.New("db failure"), @@ -1135,23 +1375,45 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { err: acme.NewErrorISE("error retrieving external account key"), } }, - "fail/retrieve-eab-key-not-found": func(t *testing.T) test { + "fail/db.GetExternalAccountKey-not-found": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "unknown-key-id", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { - return nil, acme.NewErrorISE("error retrieving external account key") + return nil, acme.ErrNotFound }, }, ctx: ctx, @@ -1160,22 +1422,94 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewErrorISE("error loading external account key unknown-key-id"), + err: acme.NewErrorISE("error retrieving external account key"), } }, - "fail/retrieve-eab-wrong-provisioner": func(t *testing.T) test { + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return nil, errors.New("force") + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error retrieving external account key"), + } + }, + "fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockError: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), @@ -1186,22 +1520,44 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), + err: acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key: name of provisioner does not match provisioner for which the EAB key was created"), } }, "fail/eab-already-bound": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) createdAt := time.Now() boundAt := time.Now().Add(1 * time.Second) return test{ @@ -1229,16 +1585,38 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { "fail/eab-verify": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { @@ -1265,16 +1643,38 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { assert.FatalError(t, err) differentJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(differentJWK.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(differentJWK.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { @@ -1293,7 +1693,118 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match"), + err: acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match"), + } + }, + "fail/no-jwk": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorServerInternalType, "jwk expected in request context"), + } + }, + "fail/nil-jwk": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, nil) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorServerInternalType, "jwk expected in request context"), } }, } @@ -1314,6 +1825,8 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { assert.Type(t, &acme.Error{}, err) ae, _ := err.(*acme.Error) assert.Equals(t, ae.Type, tc.err.Type) + assert.Equals(t, ae.Status, tc.err.Status) + assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error()) assert.Equals(t, ae.Detail, tc.err.Detail) assert.Equals(t, ae.Identifier, tc.err.Identifier) assert.Equals(t, ae.Subproblems, tc.err.Subproblems) @@ -1334,3 +1847,292 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { }) } } + +func Test_validateEABJWS(t *testing.T) { + acmeProv := newACMEProv(t) + escProvName := url.PathEscape(acmeProv.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + type test struct { + ctx context.Context + jws *jose.JSONWebSignature + keyID string + err *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/nil-jws": func(t *testing.T) test { + return test{ + jws: nil, + err: acme.NewErrorISE("no JWS provided"), + } + }, + "fail/invalid-number-of-signatures": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures = append(parsedEABJWS.Signatures, jose.Signature{}) + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "JWS must have one signature"), + } + }, + "fail/invalid-algorithm": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures[0].Protected.Algorithm = "HS42" + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm 'HS42'"), + } + }, + "fail/kid-not-set": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures[0].Protected.KeyID = "" + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'kid' field is required"), + } + }, + "fail/nonce-not-empty": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures[0].Protected.Nonce = "some-bogus-nonce" + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present"), + } + }, + "fail/url-not-set": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + delete(parsedEABJWS.Signatures[0].Protected.ExtraHeaders, "url") + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field is required"), + } + }, + "fail/no-outer-jws": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, nil) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewErrorISE("could not retrieve outer JWS from context"), + } + }, + "fail/outer-jws-multiple-signatures": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + outerJWS.Signatures = append(outerJWS.Signatures, jose.Signature{}) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature"), + } + }, + "fail/outer-jws-no-url": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS"), + } + }, + "fail/outer-jws-with-different-url": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", "this-is-not-the-same-url-as-in-the-eab-jws") + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS"), + } + }, + "ok": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + keyID: "eakID", + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + keyID, err := validateEABJWS(tc.ctx, tc.jws) + wantErr := tc.err != nil + gotErr := err != nil + if wantErr != gotErr { + t.Errorf("validateEABJWS() error = %v, want %v", err, tc.err) + } + if wantErr { + assert.NotNil(t, err) + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Status, err.Status) + assert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error()) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, tc.err.Identifier, err.Identifier) + assert.Equals(t, tc.err.Subproblems, err.Subproblems) + } else { + assert.Nil(t, err) + assert.Equals(t, tc.keyID, keyID) + } + }) + } +}