From fd9845e9c7b809fdad42ac131f8201ca5fddc000 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 24 Jan 2022 14:03:56 +0100 Subject: [PATCH] Add cursor and limit to ACME EAB DB interface --- acme/api/account.go | 148 ----- acme/api/account_test.go | 1052 ----------------------------- acme/api/eab.go | 155 +++++ acme/api/eab_test.go | 1068 ++++++++++++++++++++++++++++++ acme/db.go | 12 +- acme/db/nosql/eab.go | 12 +- acme/db/nosql/eab_test.go | 5 +- authority/admin/api/acme.go | 27 +- authority/admin/api/acme_test.go | 78 ++- authority/admin/api/handler.go | 8 +- ca/adminClient.go | 43 -- 11 files changed, 1313 insertions(+), 1295 deletions(-) create mode 100644 acme/api/eab.go create mode 100644 acme/api/eab_test.go diff --git a/acme/api/account.go b/acme/api/account.go index bf478d2a..0dc8ab40 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "net/http" @@ -9,16 +8,8 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/logging" - "go.step.sm/crypto/jose" ) -// ExternalAccountBinding represents the ACME externalAccountBinding JWS -type ExternalAccountBinding struct { - Protected string `json:"protected"` - Payload string `json:"payload"` - Sig string `json:"signature"` -} - // NewAccountRequest represents the payload for a new account request. type NewAccountRequest struct { Contact []string `json:"contact"` @@ -241,142 +232,3 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { api.JSON(w, orders) logOrdersByAccount(w, orders) } - -// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account. -func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { - acmeProv, err := acmeProvisionerFromContext(ctx) - if err != nil { - return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context") - } - - if !acmeProv.RequireEAB { - return nil, nil - } - - if nar.ExternalAccountBinding == nil { - return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") - } - - eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) - if err != nil { - return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes") - } - - eabJWS, err := jose.ParseJWS(string(eabJSONBytes)) - if err != nil { - return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") - } - - // 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 - } - - externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) - if err != nil { - if _, ok := err.(*acme.Error); ok { - return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") - } - return nil, acme.WrapErrorISE(err, "error retrieving external account key") - } - - if externalAccountKey.AlreadyBound() { - return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) - } - - payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) - if err != nil { - return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") - } - - jwk, err := jwkFromContext(ctx) - if err != nil { - return nil, err - } - - var payloadJWK *jose.JSONWebKey - if err = json.Unmarshal(payload, &payloadJWK); err != nil { - return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") - } - - if !keysAreEqual(jwk, payloadJWK) { - return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match") - } - - return externalAccountKey, nil -} - -// keysAreEqual performs an equality check on two JWKs by comparing -// the (base64 encoding) of the Key IDs. -func keysAreEqual(x, y *jose.JSONWebKey) bool { - if x == nil || y == nil { - return false - } - digestX, errX := acme.KeyToID(x) - digestY, errY := acme.KeyToID(y) - if errX != nil || errY != nil { - return false - } - 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 e54e3c1a..4c3404ec 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -993,1055 +993,3 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { }) } } - -func Test_keysAreEqual(t *testing.T) { - jwkX, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - assert.FatalError(t, err) - jwkY, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - assert.FatalError(t, err) - wrongJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - assert.FatalError(t, err) - wrongJWK.Key = struct{}{} - type args struct { - x *jose.JSONWebKey - y *jose.JSONWebKey - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "ok/nil", - args: args{ - x: jwkX, - y: nil, - }, - want: false, - }, - { - name: "ok/equal", - args: args{ - x: jwkX, - y: jwkX, - }, - want: true, - }, - { - name: "ok/not-equal", - args: args{ - x: jwkX, - y: jwkY, - }, - want: false, - }, - { - name: "ok/wrong-key-type", - args: args{ - x: wrongJWK, - y: jwkY, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := keysAreEqual(tt.args.x, tt.args.y); got != tt.want { - t.Errorf("keysAreEqual() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestHandler_validateExternalAccountBinding(t *testing.T) { - acmeProv := newACMEProv(t) - escProvName := url.PathEscape(acmeProv.GetName()) - baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - provID := acmeProv.GetID() - type test struct { - db acme.DB - ctx context.Context - nar *NewAccountRequest - eak *acme.ExternalAccountKey - err *acme.Error - } - var tests = map[string]func(t *testing.T) test{ - "ok/no-eab-required-but-provided": 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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &eab) - assert.FatalError(t, err) - prov := newACMEProv(t) - ctx := context.WithValue(context.Background(), jwkContextKey, jwk) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ctx = context.WithValue(ctx, provisionerContextKey, prov) - return test{ - db: &acme.MockDB{}, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: nil, - err: nil, - } - }, - "ok/eab": 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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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{ - MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { - return &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerID: provID, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, - }, nil - }, - }, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerID: provID, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, - }, - err: nil, - } - }, - "fail/acmeProvisionerFromContext": 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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &eab) - assert.FatalError(t, err) - eab.Payload += "{}" - 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) - return test{ - db: &acme.MockDB{}, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: nil, - err: acme.NewErrorISE("error parsing externalAccountBinding jws"), - } - }, - "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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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"), - }, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: nil, - err: acme.NewErrorISE("error retrieving external account key"), - } - }, - "fail/db.GetExternalAccountKey-not-found": 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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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.ErrNotFound - }, - }, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: nil, - err: acme.NewErrorISE("error retrieving external account key"), - } - }, - "fail/db.GetExternalAccountKey-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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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"), - }, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: nil, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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{ - db: &acme.MockDB{ - MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { - return &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerID: provID, - Reference: "testeak", - CreatedAt: createdAt, - AccountID: "some-account-id", - BoundAt: boundAt, - }, nil - }, - }, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: nil, - err: acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", "eakID", "some-account-id", boundAt), - } - }, - "fail/eab-verify": 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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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 &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerID: provID, - Reference: "testeak", - KeyBytes: []byte{1, 2, 3, 4}, - CreatedAt: time.Now(), - }, nil - }, - }, - ctx: ctx, - nar: &NewAccountRequest{ - Contact: []string{"foo", "bar"}, - ExternalAccountBinding: eab, - }, - eak: nil, - err: acme.NewErrorISE("error verifying externalAccountBinding signature"), - } - }, - "fail/eab-non-matching-keys": func(t *testing.T) test { - jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - assert.FatalError(t, err) - differentJWK, 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) - rawEABJWS, err := createRawEABJWS(differentJWK, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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 &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerID: provID, - 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.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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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", - ProvisionerID: provID, - 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) - rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(rawEABJWS, &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", - ProvisionerID: provID, - 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"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - h := &Handler{ - db: tc.db, - } - got, err := h.validateExternalAccountBinding(tc.ctx, tc.nar) - wantErr := tc.err != nil - gotErr := err != nil - if wantErr != gotErr { - t.Errorf("Handler.validateExternalAccountBinding() error = %v, want %v", err, tc.err) - } - if wantErr { - assert.NotNil(t, err) - 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) - } else { - if got == nil { - assert.Nil(t, tc.eak) - } else { - assert.NotNil(t, tc.eak) - assert.Equals(t, got.ID, tc.eak.ID) - assert.Equals(t, got.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID) - assert.Equals(t, got.Reference, tc.eak.Reference) - assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, got.AccountID, tc.eak.AccountID) - assert.Equals(t, got.BoundAt, tc.eak.BoundAt) - } - } - }) - } -} - -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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eabJWS.Signatures = append(eabJWS.Signatures, jose.Signature{}) - return test{ - jws: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eabJWS.Signatures[0].Protected.Algorithm = "HS42" - return test{ - jws: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eabJWS.Signatures[0].Protected.KeyID = "" - return test{ - jws: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - eabJWS.Signatures[0].Protected.Nonce = "some-bogus-nonce" - return test{ - jws: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - delete(eabJWS.Signatures[0].Protected.ExtraHeaders, "url") - return test{ - jws: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - ctx := context.WithValue(context.TODO(), jwsContextKey, nil) - return test{ - ctx: ctx, - jws: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - rawEABJWS := eabJWS.FullSerialize() - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal([]byte(rawEABJWS), &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)) - 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: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - rawEABJWS := eabJWS.FullSerialize() - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal([]byte(rawEABJWS), &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)) - 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: eabJWS, - 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) - url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - rawEABJWS := eabJWS.FullSerialize() - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal([]byte(rawEABJWS), &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", "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: eabJWS, - 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 := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) - assert.FatalError(t, err) - rawEABJWS := eabJWS.FullSerialize() - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal([]byte(rawEABJWS), &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) - outerJWS, err := jose.ParseJWS(raw) - assert.FatalError(t, err) - ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) - return test{ - ctx: ctx, - jws: eabJWS, - 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) - } - }) - } -} diff --git a/acme/api/eab.go b/acme/api/eab.go new file mode 100644 index 00000000..3660d066 --- /dev/null +++ b/acme/api/eab.go @@ -0,0 +1,155 @@ +package api + +import ( + "context" + "encoding/json" + + "github.com/smallstep/certificates/acme" + "go.step.sm/crypto/jose" +) + +// ExternalAccountBinding represents the ACME externalAccountBinding JWS +type ExternalAccountBinding struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` +} + +// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account. +func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { + acmeProv, err := acmeProvisionerFromContext(ctx) + if err != nil { + return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context") + } + + if !acmeProv.RequireEAB { + return nil, nil + } + + if nar.ExternalAccountBinding == nil { + return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") + } + + eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) + if err != nil { + return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes") + } + + eabJWS, err := jose.ParseJWS(string(eabJSONBytes)) + if err != nil { + return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") + } + + // 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 + } + + externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) + if err != nil { + if _, ok := err.(*acme.Error); ok { + return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") + } + return nil, acme.WrapErrorISE(err, "error retrieving external account key") + } + + if externalAccountKey.AlreadyBound() { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) + } + + payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) + if err != nil { + return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") + } + + jwk, err := jwkFromContext(ctx) + if err != nil { + return nil, err + } + + var payloadJWK *jose.JSONWebKey + if err = json.Unmarshal(payload, &payloadJWK); err != nil { + return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") + } + + if !keysAreEqual(jwk, payloadJWK) { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match") + } + + return externalAccountKey, nil +} + +// keysAreEqual performs an equality check on two JWKs by comparing +// the (base64 encoding) of the Key IDs. +func keysAreEqual(x, y *jose.JSONWebKey) bool { + if x == nil || y == nil { + return false + } + digestX, errX := acme.KeyToID(x) + digestY, errY := acme.KeyToID(y) + if errX != nil || errY != nil { + return false + } + 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/eab_test.go b/acme/api/eab_test.go new file mode 100644 index 00000000..dce9f36d --- /dev/null +++ b/acme/api/eab_test.go @@ -0,0 +1,1068 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/crypto/jose" +) + +func Test_keysAreEqual(t *testing.T) { + jwkX, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + jwkY, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + wrongJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + wrongJWK.Key = struct{}{} + type args struct { + x *jose.JSONWebKey + y *jose.JSONWebKey + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "ok/nil", + args: args{ + x: jwkX, + y: nil, + }, + want: false, + }, + { + name: "ok/equal", + args: args{ + x: jwkX, + y: jwkX, + }, + want: true, + }, + { + name: "ok/not-equal", + args: args{ + x: jwkX, + y: jwkY, + }, + want: false, + }, + { + name: "ok/wrong-key-type", + args: args{ + x: wrongJWK, + y: jwkY, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := keysAreEqual(tt.args.x, tt.args.y); got != tt.want { + t.Errorf("keysAreEqual() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHandler_validateExternalAccountBinding(t *testing.T) { + acmeProv := newACMEProv(t) + escProvName := url.PathEscape(acmeProv.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + provID := acmeProv.GetID() + type test struct { + db acme.DB + ctx context.Context + nar *NewAccountRequest + eak *acme.ExternalAccountKey + err *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok/no-eab-required-but-provided": 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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + prov := newACMEProv(t) + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + return test{ + db: &acme.MockDB{}, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: nil, + } + }, + "ok/eab": 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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, + err: nil, + } + }, + "fail/acmeProvisionerFromContext": 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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + eab.Payload += "{}" + 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) + return test{ + db: &acme.MockDB{}, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error parsing externalAccountBinding jws"), + } + }, + "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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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"), + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error retrieving external account key"), + } + }, + "fail/db.GetExternalAccountKey-not-found": 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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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.ErrNotFound + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error retrieving external account key"), + } + }, + "fail/db.GetExternalAccountKey-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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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"), + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + CreatedAt: createdAt, + AccountID: "some-account-id", + BoundAt: boundAt, + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", "eakID", "some-account-id", boundAt), + } + }, + "fail/eab-verify": 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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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 &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 2, 3, 4}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error verifying externalAccountBinding signature"), + } + }, + "fail/eab-non-matching-keys": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + differentJWK, 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) + rawEABJWS, err := createRawEABJWS(differentJWK, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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 &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + 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.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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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", + ProvisionerID: provID, + 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) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &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", + ProvisionerID: provID, + 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"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + db: tc.db, + } + got, err := h.validateExternalAccountBinding(tc.ctx, tc.nar) + wantErr := tc.err != nil + gotErr := err != nil + if wantErr != gotErr { + t.Errorf("Handler.validateExternalAccountBinding() error = %v, want %v", err, tc.err) + } + if wantErr { + assert.NotNil(t, err) + 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) + } else { + if got == nil { + assert.Nil(t, tc.eak) + } else { + assert.NotNil(t, tc.eak) + assert.Equals(t, got.ID, tc.eak.ID) + assert.Equals(t, got.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, got.Reference, tc.eak.Reference) + assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, got.AccountID, tc.eak.AccountID) + assert.Equals(t, got.BoundAt, tc.eak.BoundAt) + } + } + }) + } +} + +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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures = append(eabJWS.Signatures, jose.Signature{}) + return test{ + jws: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures[0].Protected.Algorithm = "HS42" + return test{ + jws: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures[0].Protected.KeyID = "" + return test{ + jws: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures[0].Protected.Nonce = "some-bogus-nonce" + return test{ + jws: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + delete(eabJWS.Signatures[0].Protected.ExtraHeaders, "url") + return test{ + jws: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, nil) + return test{ + ctx: ctx, + jws: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &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)) + 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: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &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)) + 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: eabJWS, + 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) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &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", "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: eabJWS, + 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 := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &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) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: eabJWS, + 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) + } + }) + } +} diff --git a/acme/db.go b/acme/db.go index 708bce9e..412276fd 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,7 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) - GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error) + GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error @@ -58,7 +58,7 @@ type MockDB struct { MockCreateExternalAccountKey func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) - MockGetExternalAccountKeys func(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error) + MockGetExternalAccountKeys func(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error @@ -149,13 +149,13 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID } // GetExternalAccountKeys mock -func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) { if m.MockGetExternalAccountKeys != nil { - return m.MockGetExternalAccountKeys(ctx, provisionerID) + return m.MockGetExternalAccountKeys(ctx, provisionerID, cursor, limit) } else if m.MockError != nil { - return nil, m.MockError + return nil, "", m.MockError } - return m.MockRet1.([]*ExternalAccountKey), m.MockError + return m.MockRet1.([]*ExternalAccountKey), "", m.MockError } // GetExternalAccountKeyByReference mock diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go index 51f0e053..170f457d 100644 --- a/acme/db/nosql/eab.go +++ b/acme/db/nosql/eab.go @@ -159,20 +159,22 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID } // GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner -func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { externalAccountKeyMutex.RLock() defer externalAccountKeyMutex.RUnlock() + // cursor and limit are ignored in open source, at least for now. + var eakIDs []string r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) if err != nil { if !nosqlDB.IsErrNotFound(err) { - return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) + return nil, "", errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) } // it may happen that no record is found; we'll continue with an empty slice } else { if err := json.Unmarshal(r, &eakIDs); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) + return nil, "", errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) } } @@ -184,7 +186,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) eak, err := db.getDBExternalAccountKey(ctx, eakID) if err != nil { if !nosqlDB.IsErrNotFound(err) { - return nil, errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID) + return nil, "", errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID) } } keys = append(keys, &acme.ExternalAccountKey{ @@ -198,7 +200,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) }) } - return keys, nil + return keys, "", nil } // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference diff --git a/acme/db/nosql/eab_test.go b/acme/db/nosql/eab_test.go index 6db0b734..be14d90b 100644 --- a/acme/db/nosql/eab_test.go +++ b/acme/db/nosql/eab_test.go @@ -576,7 +576,9 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - if eaks, err := d.GetExternalAccountKeys(context.Background(), provID); err != nil { + cursor, limit := "", 0 + if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil { + assert.Equals(t, "", nextCursor) switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -593,6 +595,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { } } else if assert.Nil(t, tc.err) { assert.Equals(t, len(eaks), len(tc.eaks)) + assert.Equals(t, "", nextCursor) for i, eak := range eaks { assert.Equals(t, eak.ID, tc.eaks[i].ID) assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 82850010..2cd75900 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -35,7 +35,8 @@ func (r *CreateExternalAccountKeyRequest) Validate() error { // GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses type GetExternalAccountKeysResponse struct { - EAKs []*linkedca.EABKey `json:"eaks"` + EAKs []*linkedca.EABKey `json:"eaks"` + NextCursor string `json:"nextCursor"` } // requireEABEnabled is a middleware that ensures ACME EAB is enabled @@ -43,7 +44,7 @@ type GetExternalAccountKeysResponse struct { func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - provName := chi.URLParam(r, "prov") + provName := chi.URLParam(r, "provisionerName") eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName) if err != nil { api.WriteError(w, err) @@ -187,9 +188,12 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { var ( - key *acme.ExternalAccountKey - keys []*acme.ExternalAccountKey - err error + key *acme.ExternalAccountKey + keys []*acme.ExternalAccountKey + err error + cursor string + nextCursor string + limit int ) ctx := r.Context() @@ -199,7 +203,13 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) return } - reference := chi.URLParam(r, "ref") + if cursor, limit, err = api.ParseCursor(r); err != nil { + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, + "error parsing cursor and limit from query params")) + return + } + + reference := chi.URLParam(r, "reference") if reference != "" { if key, err = h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference)) @@ -209,7 +219,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) keys = []*acme.ExternalAccountKey{key} } } else { - if keys, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId()); err != nil { + if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId(), cursor, limit); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) return } @@ -230,6 +240,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) } api.JSON(w, &GetExternalAccountKeysResponse{ - EAKs: eaks, + EAKs: eaks, + NextCursor: nextCursor, }) } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 475e6606..50086955 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -46,7 +46,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/h.provisionerHasEABEnabled": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { @@ -65,7 +65,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { }, "ok/eab-disabled": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { @@ -105,7 +105,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { }, "ok/eab-enabled": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { @@ -498,7 +498,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/ReadJSON": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) body := []byte("{!?}") return test{ @@ -516,7 +516,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "fail/validate": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) req := CreateExternalAccountKeyRequest{ Reference: strings.Repeat("A", 257), @@ -538,7 +538,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "fail/no-provisioner-in-context": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) req := CreateExternalAccountKeyRequest{ Reference: "aRef", @@ -560,7 +560,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ @@ -591,7 +591,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "fail/reference-conflict-409": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ @@ -629,7 +629,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "fail/acmeDB.CreateExternalAccountKey-no-reference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ @@ -659,7 +659,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "fail/acmeDB.CreateExternalAccountKey-with-reference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ @@ -694,7 +694,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "ok/no-reference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ @@ -731,7 +731,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, "ok/with-reference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ @@ -831,7 +831,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-provisioner-in-context": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) return test{ ctx: ctx, @@ -846,7 +846,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { }, "fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") chiCtx.URLParams.Add("id", "keyID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) @@ -871,7 +871,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { }, "ok": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") chiCtx.URLParams.Add("id", "keyID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) @@ -948,7 +948,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { var tests = map[string]func(t *testing.T) test{ "fail/no-provisioner-in-context": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) req := httptest.NewRequest("GET", "/foo", nil) return test{ @@ -963,10 +963,28 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { }, } }, + "fail/parse-cursor": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + req := httptest.NewRequest("GET", "/foo?limit=A", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + return test{ + ctx: ctx, + statusCode: 400, + req: req, + err: &admin.Error{ + Status: 400, + Type: admin.ErrorBadRequestType.String(), + Detail: "bad request", + Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax", + }, + } + }, "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") - chiCtx.URLParams.Add("ref", "an-external-key-reference") + chiCtx.URLParams.Add("provisionerName", "provName") + chiCtx.URLParams.Add("reference", "an-external-key-reference") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) @@ -992,14 +1010,16 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { }, "fail/acmeDB.GetExternalAccountKeys": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { assert.Equals(t, "provID", provisionerID) - return nil, errors.New("force") + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") }, } return test{ @@ -1017,8 +1037,8 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { }, "ok/reference-not-found": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") - chiCtx.URLParams.Add("ref", "an-external-key-reference") + chiCtx.URLParams.Add("provisionerName", "provName") + chiCtx.URLParams.Add("reference", "an-external-key-reference") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) @@ -1042,8 +1062,8 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { }, "ok/reference-found": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") - chiCtx.URLParams.Add("ref", "an-external-key-reference") + chiCtx.URLParams.Add("provisionerName", "provName") + chiCtx.URLParams.Add("reference", "an-external-key-reference") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) @@ -1082,7 +1102,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { }, "ok/multiple-keys": func(t *testing.T) test { chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("provisionerName", "provName") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = context.WithValue(ctx, provisionerContextKey, prov) @@ -1090,8 +1110,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { var boundAt time.Time boundAtSet := time.Now().Add(-12 * time.Hour) db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { assert.Equals(t, "provID", provisionerID) + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) return []*acme.ExternalAccountKey{ { ID: "eakID1", @@ -1116,7 +1138,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { BoundAt: boundAtSet, AccountID: "accountID", }, - }, nil + }, "", nil }, } return test{ diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index fcdb626b..51751057 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -47,8 +47,8 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) // ACME External Account Binding Keys - r.MethodFunc("GET", "/acme/eab/{prov}/{ref}", authnz(requireEABEnabled(h.GetExternalAccountKeys))) - r.MethodFunc("GET", "/acme/eab/{prov}", authnz(requireEABEnabled(h.GetExternalAccountKeys))) - r.MethodFunc("POST", "/acme/eab/{prov}", authnz(requireEABEnabled(h.CreateExternalAccountKey))) - r.MethodFunc("DELETE", "/acme/eab/{prov}/{id}", authnz(requireEABEnabled(h.DeleteExternalAccountKey))) + r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.GetExternalAccountKeys))) + r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.GetExternalAccountKeys))) + r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.CreateExternalAccountKey))) + r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.DeleteExternalAccountKey))) } diff --git a/ca/adminClient.go b/ca/adminClient.go index ab7c3bbb..cfbf595a 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -667,49 +667,6 @@ retry: return nil } -// GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeys(provisionerName, reference string, opts ...AdminOption) ([]*linkedca.EABKey, error) { - var retried bool - o := new(adminOptions) - if err := o.apply(opts); err != nil { - return nil, err - } - p := path.Join(adminURLPrefix, "acme/eab", provisionerName) - if reference != "" { - p = path.Join(p, "/", reference) - } - u := c.endpoint.ResolveReference(&url.URL{ - Path: p, - RawQuery: o.rawQuery(), - }) - tok, err := c.generateAdminToken(u.Path) - if err != nil { - return nil, errors.Wrapf(err, "error generating admin token") - } - req, err := http.NewRequest("GET", u.String(), http.NoBody) - if err != nil { - return nil, errors.Wrapf(err, "create GET %s request failed", u) - } - req.Header.Add("Authorization", tok) -retry: - resp, err := c.client.Do(req) - if err != nil { - return nil, errors.Wrapf(err, "client GET %s failed", u) - } - if resp.StatusCode >= 400 { - if !retried && c.retryOnError(resp) { - retried = true - goto retry - } - return nil, readAdminError(resp.Body) - } - var body = new(adminAPI.GetExternalAccountKeysResponse) - if err := readJSON(resp.Body, body); err != nil { - return nil, errors.Wrapf(err, "error reading %s", u) - } - return body.EAKs, nil -} - func readAdminError(r io.ReadCloser) error { // TODO: not all errors can be read (i.e. 404); seems to be a bigger issue defer r.Close()