From f81d49d9632d83f48803eb65e348d954b9fa397c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 17 Jul 2021 17:35:44 +0200 Subject: [PATCH 01/47] Add first working version of External Account Binding --- acme/account.go | 10 ++++ acme/api/account.go | 83 +++++++++++++++++++++++++++++-- acme/api/handler.go | 23 +++++++++ acme/db.go | 26 ++++++++++ acme/db/nosql/account.go | 68 +++++++++++++++++++++++++ acme/db/nosql/nosql.go | 19 +++---- authority/admin/api/eak.go | 45 +++++++++++++++++ authority/admin/api/handler.go | 3 ++ authority/admin/db.go | 13 +++++ authority/admin/db/nosql/eak.go | 51 +++++++++++++++++++ authority/admin/db/nosql/nosql.go | 7 +-- authority/admin/eak/eak.go | 12 +++++ authority/provisioner/acme.go | 15 +++--- 13 files changed, 353 insertions(+), 22 deletions(-) create mode 100644 authority/admin/api/eak.go create mode 100644 authority/admin/db/nosql/eak.go create mode 100644 authority/admin/eak/eak.go diff --git a/acme/account.go b/acme/account.go index 197a3400..3fc0d451 100644 --- a/acme/account.go +++ b/acme/account.go @@ -4,6 +4,7 @@ import ( "crypto" "encoding/base64" "encoding/json" + "time" "go.step.sm/crypto/jose" ) @@ -40,3 +41,12 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { } return base64.RawURLEncoding.EncodeToString(kid), nil } + +type ExternalAccountKey struct { + ID string `json:"id"` + Name string `json:"name"` + AccountID string `json:"-"` + KeyBytes []byte `json:"-"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt,omitempty"` +} diff --git a/acme/api/account.go b/acme/api/account.go index b733c679..17d7bb52 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -1,20 +1,26 @@ package api import ( + "bytes" + "context" "encoding/json" "net/http" "github.com/go-chi/chi" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" + + squarejose "gopkg.in/square/go-jose.v2" ) // NewAccountRequest represents the payload for a new account request. type NewAccountRequest struct { - Contact []string `json:"contact"` - OnlyReturnExisting bool `json:"onlyReturnExisting"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + Contact []string `json:"contact"` + OnlyReturnExisting bool `json:"onlyReturnExisting"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"` } func validateContacts(cs []string) error { @@ -83,6 +89,14 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } + if err := h.validateExternalAccountBinding(ctx, &nar); err != nil { + api.WriteError(w, err) + return + } + + // TODO: link account to the key when created; mark boundat timestamp + // TODO: return the externalAccountBinding field (should contain same info) if new account created + httpStatus := http.StatusCreated acc, err := accountFromContext(r.Context()) if err != nil { @@ -205,3 +219,66 @@ 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) error { + + prov, err := provisionerFromContext(ctx) + if err != nil { + return err + } + + acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner + if !ok || acmeProv == nil { + return acme.NewErrorISE("provisioner in context is not an ACME provisioner") + } + + shouldSkipAccountBindingValidation := !acmeProv.RequireEAB + if shouldSkipAccountBindingValidation { + return nil + } + + if nar.ExternalAccountBinding == nil { + return acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") + } + + eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) + if err != nil { + return acme.WrapErrorISE(err, "error marshalling externalAccountBinding") + } + + eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) + if err != nil { + return acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") + } + + // TODO: verify supported algorithms against the incoming alg (and corresponding settings)? + // TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration + + keyID := eabJWS.Signatures[0].Protected.KeyID + externalAccountKey, err := h.db.GetExternalAccountKey(ctx, keyID) + if err != nil { + return acme.WrapErrorISE(err, "error retrieving external account key") + } + + payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) + if err != nil { + return acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") + } + + jwk, err := jwkFromContext(ctx) + if err != nil { + return err + } + + jwkJSONBytes, err := jwk.MarshalJSON() + if err != nil { + return acme.WrapErrorISE(err, "error marshaling jwk") + } + + if bytes.Equal(payload, jwkJSONBytes) { + acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use + } + + return nil +} diff --git a/acme/api/handler.go b/acme/api/handler.go index 2a6d3a02..4519bd38 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -123,6 +123,13 @@ func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) { } } +type Meta struct { + TermsOfService string `json:"termsOfService,omitempty"` + Website string `json:"website,omitempty"` + CaaIdentities []string `json:"caaIdentities,omitempty"` + ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"` +} + // Directory represents an ACME directory for configuring clients. type Directory struct { NewNonce string `json:"newNonce"` @@ -130,6 +137,7 @@ type Directory struct { NewOrder string `json:"newOrder"` RevokeCert string `json:"revokeCert"` KeyChange string `json:"keyChange"` + Meta Meta `json:"meta"` } // ToLog enables response logging for the Directory type. @@ -145,12 +153,27 @@ func (d *Directory) ToLog() (interface{}, error) { // for client configuration. func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + prov, err := provisionerFromContext(ctx) + if err != nil { + api.WriteError(w, err) + return + } + + acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner + if !ok || acmeProv == nil { + api.WriteError(w, acme.NewErrorISE("provisioner in context is not an ACME provisioner")) + return + } + api.JSON(w, &Directory{ NewNonce: h.linker.GetLink(ctx, NewNonceLinkType), NewAccount: h.linker.GetLink(ctx, NewAccountLinkType), NewOrder: h.linker.GetLink(ctx, NewOrderLinkType), RevokeCert: h.linker.GetLink(ctx, RevokeCertLinkType), KeyChange: h.linker.GetLink(ctx, KeyChangeLinkType), + Meta: Meta{ + ExternalAccountRequired: acmeProv.RequireEAB, + }, }) } diff --git a/acme/db.go b/acme/db.go index d678fef4..5aae131a 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,6 +19,9 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error + CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) + GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) + CreateNonce(ctx context.Context) (Nonce, error) DeleteNonce(ctx context.Context, nonce Nonce) error @@ -47,6 +50,9 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error + MockCreateExternalAccountKey func(ctx context.Context, name string) (*ExternalAccountKey, error) + MockGetExternalAccountKey func(ctx context.Context, keyID string) (*ExternalAccountKey, error) + MockCreateNonce func(ctx context.Context) (Nonce, error) MockDeleteNonce func(ctx context.Context, nonce Nonce) error @@ -110,6 +116,26 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error { return m.MockError } +// CreateExternalAccountKey mock +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) { + if m.MockCreateExternalAccountKey != nil { + return m.MockCreateExternalAccountKey(ctx, name) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(*ExternalAccountKey), m.MockError +} + +// GetExternalAccountKey mock +func (m *MockDB) GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) { + if m.MockGetExternalAccountKey != nil { + return m.MockGetExternalAccountKey(ctx, keyID) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(*ExternalAccountKey), m.MockError +} + // CreateNonce mock func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) { if m.MockCreateNonce != nil { diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 1c3bec5d..466adf43 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -2,6 +2,7 @@ package nosql import ( "context" + "crypto/rand" "encoding/json" "time" @@ -26,6 +27,15 @@ func (dba *dbAccount) clone() *dbAccount { return &nu } +type dbExternalAccountKey struct { + ID string `json:"id"` + Name string `json:"name"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key,omitempty"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` +} + func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) { id, err := db.db.Get(accountByKeyIDTable, []byte(kid)) if err != nil { @@ -134,3 +144,61 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { return db.save(ctx, old.ID, nu, old, "account", accountTable) } + +// CreateExternalAccountKey creates a new External Account Binding key with a name +func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.ExternalAccountKey, error) { + keyID, err := randID() + if err != nil { + return nil, err + } + + random := make([]byte, 32) + _, err = rand.Read(random) + if err != nil { + return nil, err + } + + dbeak := &dbExternalAccountKey{ + ID: keyID, + Name: name, + KeyBytes: random, + CreatedAt: clock.Now(), + } + + if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { + return nil, err + } + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} + +// GetExternalAccountKey retrieves an External Account Binding key by KeyID +func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.ExternalAccountKey, error) { + data, err := db.db.Get(externalAccountKeyTable, []byte(keyID)) + if err != nil { + if nosqlDB.IsErrNotFound(err) { + return nil, acme.ErrNotFound + } + return nil, errors.Wrapf(err, "error loading external account key %s", keyID) + } + + dbeak := new(dbExternalAccountKey) + if err = json.Unmarshal(data, dbeak); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", keyID) + } + + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 052f5729..320e7d58 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -11,14 +11,15 @@ import ( ) var ( - accountTable = []byte("acme_accounts") - accountByKeyIDTable = []byte("acme_keyID_accountID_index") - authzTable = []byte("acme_authzs") - challengeTable = []byte("acme_challenges") - nonceTable = []byte("nonces") - orderTable = []byte("acme_orders") - ordersByAccountIDTable = []byte("acme_account_orders_index") - certTable = []byte("acme_certs") + accountTable = []byte("acme_accounts") + accountByKeyIDTable = []byte("acme_keyID_accountID_index") + authzTable = []byte("acme_authzs") + challengeTable = []byte("acme_challenges") + nonceTable = []byte("nonces") + orderTable = []byte("acme_orders") + ordersByAccountIDTable = []byte("acme_account_orders_index") + certTable = []byte("acme_certs") + externalAccountKeyTable = []byte("acme_external_account_keys") ) // DB is a struct that implements the AcmeDB interface. @@ -29,7 +30,7 @@ type DB struct { // New configures and returns a new ACME DB backend implemented using a nosql DB. func New(db nosqlDB.DB) (*DB, error) { tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, - challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable} + challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, externalAccountKeyTable} for _, b := range tables { if err := db.CreateTable(b); err != nil { return nil, errors.Wrapf(err, "error creating table %s", diff --git a/authority/admin/api/eak.go b/authority/admin/api/eak.go new file mode 100644 index 00000000..1e46ff31 --- /dev/null +++ b/authority/admin/api/eak.go @@ -0,0 +1,45 @@ +package api + +import ( + "net/http" + + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" +) + +// CreateExternalAccountKeyRequest is the type for GET /admin/eak requests +type CreateExternalAccountKeyRequest struct { + Name string `json:"name"` +} + +// CreateExternalAccountKeyResponse is the type for GET /admin/eak responses +type CreateExternalAccountKeyResponse struct { + KeyID string `json:"keyID"` + Name string `json:"name"` + Key []byte `json:"key"` +} + +// CreateExternalAccountKey creates a new External Account Binding key +func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { + var eakRequest = new(CreateExternalAccountKeyRequest) + if err := api.ReadJSON(r.Body, eakRequest); err != nil { // TODO: rewrite into protobuf json (likely) + api.WriteError(w, err) + return + } + + // TODO: Validate input + + eak, err := h.db.CreateExternalAccountKey(r.Context(), eakRequest.Name) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", eakRequest.Name)) + return + } + + eakResponse := CreateExternalAccountKeyResponse{ + KeyID: eak.ID, + Name: eak.Name, + Key: eak.KeyBytes, + } + + api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely) +} diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index d88edfa1..561db687 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -38,4 +38,7 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("POST", "/admins", authnz(h.CreateAdmin)) r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin)) r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) + + // External Account Binding Keys + r.MethodFunc("POST", "/eak", h.CreateExternalAccountKey) // TODO: authnz } diff --git a/authority/admin/db.go b/authority/admin/db.go index 15fe6686..14520207 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -7,6 +7,8 @@ import ( "github.com/pkg/errors" "go.step.sm/linkedca" + + "github.com/smallstep/certificates/authority/admin/eak" ) const ( @@ -67,6 +69,8 @@ type DB interface { GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error DeleteAdmin(ctx context.Context, id string) error + + CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) } // MockDB is an implementation of the DB interface that should only be used as @@ -84,6 +88,8 @@ type MockDB struct { MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error MockDeleteAdmin func(ctx context.Context, id string) error + MockCreateExternalAccountKey func(ctx context.Context, name string) (*eak.ExternalAccountKey, error) + MockError error MockRet1 interface{} } @@ -177,3 +183,10 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error { } return m.MockError } + +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) { + if m.MockCreateExternalAccountKey != nil { + return m.MockCreateExternalAccountKey(ctx, name) + } + return m.MockRet1.(*eak.ExternalAccountKey), m.MockError +} diff --git a/authority/admin/db/nosql/eak.go b/authority/admin/db/nosql/eak.go new file mode 100644 index 00000000..a3a3d96d --- /dev/null +++ b/authority/admin/db/nosql/eak.go @@ -0,0 +1,51 @@ +package nosql + +import ( + "context" + "crypto/rand" + "time" + + "github.com/smallstep/certificates/authority/admin/eak" +) + +type dbExternalAccountKey struct { + ID string `json:"id"` + Name string `json:"name"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key,omitempty"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` +} + +// CreateExternalAccountKey creates a new External Account Binding key +func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) { + keyID, err := randID() + if err != nil { + return nil, err + } + + random := make([]byte, 32) + _, err = rand.Read(random) + if err != nil { + return nil, err + } + + dbeak := &dbExternalAccountKey{ + ID: keyID, + Name: name, + KeyBytes: random, + CreatedAt: clock.Now(), + } + + if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { + return nil, err + } + return &eak.ExternalAccountKey{ + ID: dbeak.ID, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} diff --git a/authority/admin/db/nosql/nosql.go b/authority/admin/db/nosql/nosql.go index 18599b02..8019cbdf 100644 --- a/authority/admin/db/nosql/nosql.go +++ b/authority/admin/db/nosql/nosql.go @@ -11,8 +11,9 @@ import ( ) var ( - adminsTable = []byte("admins") - provisionersTable = []byte("provisioners") + adminsTable = []byte("admins") + provisionersTable = []byte("provisioners") + externalAccountKeyTable = []byte("acme_external_account_keys") ) // DB is a struct that implements the AdminDB interface. @@ -23,7 +24,7 @@ type DB struct { // New configures and returns a new Authority DB backend implemented using a nosql DB. func New(db nosqlDB.DB, authorityID string) (*DB, error) { - tables := [][]byte{adminsTable, provisionersTable} + tables := [][]byte{adminsTable, provisionersTable, externalAccountKeyTable} for _, b := range tables { if err := db.CreateTable(b); err != nil { return nil, errors.Wrapf(err, "error creating table %s", diff --git a/authority/admin/eak/eak.go b/authority/admin/eak/eak.go new file mode 100644 index 00000000..b2eaa157 --- /dev/null +++ b/authority/admin/eak/eak.go @@ -0,0 +1,12 @@ +package eak + +import "time" + +type ExternalAccountKey struct { + ID string `json:"id"` + Name string `json:"name"` + AccountID string `json:"-"` + KeyBytes []byte `json:"-"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt,omitempty"` +} diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index d81b0231..9a0f1356 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -13,13 +13,14 @@ import ( // provisioning flow. type ACME struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - ForceCN bool `json:"forceCN,omitempty"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + ForceCN bool `json:"forceCN,omitempty"` + RequireEAB bool `json:"requireEAB,omitempty"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer } // GetID returns the provisioner unique identifier. From d44cd18b962fb3a9ab4bcca72852c945eae7ecbe Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 17 Jul 2021 19:02:47 +0200 Subject: [PATCH 02/47] Add External Accounting Binding key "BoundAt" marking --- acme/account.go | 17 ++++++++++---- acme/api/account.go | 44 +++++++++++++++++++++-------------- acme/api/account_test.go | 1 + acme/db.go | 11 +++++++++ acme/db/nosql/account.go | 50 +++++++++++++++++++++++++++++++--------- 5 files changed, 90 insertions(+), 33 deletions(-) diff --git a/acme/account.go b/acme/account.go index 3fc0d451..0113561f 100644 --- a/acme/account.go +++ b/acme/account.go @@ -12,11 +12,12 @@ import ( // Account is a subset of the internal account type containing only those // attributes required for responses in the ACME protocol. type Account struct { - ID string `json:"-"` - Key *jose.JSONWebKey `json:"-"` - Contact []string `json:"contact,omitempty"` - Status Status `json:"status"` - OrdersURL string `json:"orders"` + ID string `json:"-"` + Key *jose.JSONWebKey `json:"-"` + Contact []string `json:"contact,omitempty"` + Status Status `json:"status"` + OrdersURL string `json:"orders"` + ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"` } // ToLog enables response logging. @@ -50,3 +51,9 @@ type ExternalAccountKey struct { CreatedAt time.Time `json:"createdAt"` BoundAt time.Time `json:"boundAt,omitempty"` } + +func (eak *ExternalAccountKey) BindTo(account *Account) { + eak.AccountID = account.ID + eak.BoundAt = time.Now() + eak.KeyBytes = []byte{} // TODO: ensure that single use keys are OK +} diff --git a/acme/api/account.go b/acme/api/account.go index 17d7bb52..4a948e91 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -89,14 +89,12 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } - if err := h.validateExternalAccountBinding(ctx, &nar); err != nil { + eak, err := h.validateExternalAccountBinding(ctx, &nar) + if err != nil { api.WriteError(w, err) return } - // TODO: link account to the key when created; mark boundat timestamp - // TODO: return the externalAccountBinding field (should contain same info) if new account created - httpStatus := http.StatusCreated acc, err := accountFromContext(r.Context()) if err != nil { @@ -128,6 +126,14 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { api.WriteError(w, acme.WrapErrorISE(err, "error creating account")) return } + if eak != nil { // means that we have a (valid) External Account Binding key that should be used + eak.BindTo(acc) + if err := h.db.UpdateExternalAccountKey(ctx, eak); err != nil { + api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key")) + return + } + acc.ExternalAccountBinding = nar.ExternalAccountBinding + } } else { // Account exists // httpStatus = http.StatusOK @@ -221,35 +227,35 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { } // validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account -func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) error { +func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { prov, err := provisionerFromContext(ctx) if err != nil { - return err + return nil, err } acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner if !ok || acmeProv == nil { - return acme.NewErrorISE("provisioner in context is not an ACME provisioner") + return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner") } shouldSkipAccountBindingValidation := !acmeProv.RequireEAB if shouldSkipAccountBindingValidation { - return nil + return nil, nil } if nar.ExternalAccountBinding == nil { - return acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") + return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") } eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) if err != nil { - return acme.WrapErrorISE(err, "error marshalling externalAccountBinding") + return nil, acme.WrapErrorISE(err, "error marshalling externalAccountBinding") } eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) if err != nil { - return acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") + return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") } // TODO: verify supported algorithms against the incoming alg (and corresponding settings)? @@ -258,27 +264,31 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc keyID := eabJWS.Signatures[0].Protected.KeyID externalAccountKey, err := h.db.GetExternalAccountKey(ctx, keyID) if err != nil { - return acme.WrapErrorISE(err, "error retrieving external account key") + return nil, acme.WrapErrorISE(err, "error retrieving external account key") + } + + if !externalAccountKey.BoundAt.IsZero() { // TODO: ensure that single use keys are OK + return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already used", keyID) } payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) if err != nil { - return acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") + return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") } jwk, err := jwkFromContext(ctx) if err != nil { - return err + return nil, err } jwkJSONBytes, err := jwk.MarshalJSON() if err != nil { - return acme.WrapErrorISE(err, "error marshaling jwk") + return nil, acme.WrapErrorISE(err, "error marshaling jwk") } if bytes.Equal(payload, jwkJSONBytes) { - acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use + return nil, acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use } - return nil + return externalAccountKey, nil } diff --git a/acme/api/account_test.go b/acme/api/account_test.go index c4d7a812..fe0f5392 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -343,6 +343,7 @@ func TestHandler_NewAccount(t *testing.T) { b, err := json.Marshal(nar) assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, provisionerContextKey, prov) return test{ ctx: ctx, statusCode: 400, diff --git a/acme/db.go b/acme/db.go index 5aae131a..b80e8f81 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,6 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) + UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error CreateNonce(ctx context.Context) (Nonce, error) DeleteNonce(ctx context.Context, nonce Nonce) error @@ -52,6 +53,7 @@ type MockDB struct { MockCreateExternalAccountKey func(ctx context.Context, name string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, keyID string) (*ExternalAccountKey, error) + MockUpdateExternalAccountKey func(ctx context.Context, eak *ExternalAccountKey) error MockCreateNonce func(ctx context.Context) (Nonce, error) MockDeleteNonce func(ctx context.Context, nonce Nonce) error @@ -136,6 +138,15 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, keyID string) (*Exte return m.MockRet1.(*ExternalAccountKey), m.MockError } +func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error { + if m.MockUpdateExternalAccountKey != nil { + return m.MockUpdateExternalAccountKey(ctx, eak) + } else if m.MockError != nil { + return m.MockError + } + return m.MockError +} + // CreateNonce mock func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) { if m.MockCreateNonce != nil { diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 466adf43..e38ff425 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -31,7 +31,7 @@ type dbExternalAccountKey struct { ID string `json:"id"` Name string `json:"name"` AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key,omitempty"` + KeyBytes []byte `json:"key"` CreatedAt time.Time `json:"createdAt"` BoundAt time.Time `json:"boundAt"` } @@ -64,6 +64,24 @@ func (db *DB) getDBAccount(ctx context.Context, id string) (*dbAccount, error) { return dbacc, nil } +// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey. +func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) { + data, err := db.db.Get(externalAccountKeyTable, []byte(id)) + if err != nil { + if nosqlDB.IsErrNotFound(err) { + return nil, acme.ErrNotFound + } + return nil, errors.Wrapf(err, "error loading external account key %s", id) + } + + dbeak := new(dbExternalAccountKey) + if err = json.Unmarshal(data, dbeak); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", id) + } + + return dbeak, nil +} + // GetAccount retrieves an ACME account by ID. func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) { dbacc, err := db.getDBAccount(ctx, id) @@ -180,17 +198,9 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme. // GetExternalAccountKey retrieves an External Account Binding key by KeyID func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.ExternalAccountKey, error) { - data, err := db.db.Get(externalAccountKeyTable, []byte(keyID)) + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { - if nosqlDB.IsErrNotFound(err) { - return nil, acme.ErrNotFound - } - return nil, errors.Wrapf(err, "error loading external account key %s", keyID) - } - - dbeak := new(dbExternalAccountKey) - if err = json.Unmarshal(data, dbeak); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", keyID) + return nil, err } return &acme.ExternalAccountKey{ @@ -202,3 +212,21 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.Ex BoundAt: dbeak.BoundAt, }, nil } + +func (db *DB) UpdateExternalAccountKey(ctx context.Context, eak *acme.ExternalAccountKey) error { + old, err := db.getDBExternalAccountKey(ctx, eak.ID) + if err != nil { + return err + } + + nu := dbExternalAccountKey{ + ID: eak.ID, + Name: eak.Name, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + } + + return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) +} From 2110c7722fe83c9c08cd91a5a127496fbb76be9c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 17 Jul 2021 20:29:12 +0200 Subject: [PATCH 03/47] Fix JWK payload key equality check --- acme/api/account.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index 4a948e91..9800db66 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -1,7 +1,6 @@ package api import ( - "bytes" "context" "encoding/json" "net/http" @@ -281,14 +280,27 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, err } - jwkJSONBytes, err := jwk.MarshalJSON() + var payloadJWK *squarejose.JSONWebKey + err = json.Unmarshal(payload, &payloadJWK) if err != nil { - return nil, acme.WrapErrorISE(err, "error marshaling jwk") + return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") } - if bytes.Equal(payload, jwkJSONBytes) { + if !keysAreEqual(jwk, payloadJWK) { return nil, acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use } return externalAccountKey, nil } + +func keysAreEqual(x, y *squarejose.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 +} From 540d5fbbdceca5ec964fae6d36b55328df00f7c4 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 17 Jul 2021 20:35:44 +0200 Subject: [PATCH 04/47] Fix marshaling -> marshalling --- acme/api/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/api/account.go b/acme/api/account.go index 9800db66..473c6d35 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -283,7 +283,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc var payloadJWK *squarejose.JSONWebKey err = json.Unmarshal(payload, &payloadJWK) if err != nil { - return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") + return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshalling payload into jwk") } if !keysAreEqual(jwk, payloadJWK) { From d669f3cb146b6a71d91488a3851957d444672fde Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 17 Jul 2021 20:39:12 +0200 Subject: [PATCH 05/47] Fix misspelling --- acme/api/account.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index 473c6d35..9a48574a 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -249,7 +249,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) if err != nil { - return nil, acme.WrapErrorISE(err, "error marshalling externalAccountBinding") + return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding") } eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) @@ -283,7 +283,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc var payloadJWK *squarejose.JSONWebKey err = json.Unmarshal(payload, &payloadJWK) if err != nil { - return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshalling payload into jwk") + return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") } if !keysAreEqual(jwk, payloadJWK) { From b65a588d5bef26b61c40cf721312687459fc1b25 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 22 Jul 2021 22:43:21 +0200 Subject: [PATCH 06/47] Make authentication work for /admin/eak --- authority/admin/api/eak.go | 12 +++++------ authority/admin/api/handler.go | 2 +- ca/adminClient.go | 38 ++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/authority/admin/api/eak.go b/authority/admin/api/eak.go index 1e46ff31..6f785f7a 100644 --- a/authority/admin/api/eak.go +++ b/authority/admin/api/eak.go @@ -7,12 +7,12 @@ import ( "github.com/smallstep/certificates/authority/admin" ) -// CreateExternalAccountKeyRequest is the type for GET /admin/eak requests +// CreateExternalAccountKeyRequest is the type for POST /admin/eak requests type CreateExternalAccountKeyRequest struct { Name string `json:"name"` } -// CreateExternalAccountKeyResponse is the type for GET /admin/eak responses +// CreateExternalAccountKeyResponse is the type for POST /admin/eak responses type CreateExternalAccountKeyResponse struct { KeyID string `json:"keyID"` Name string `json:"name"` @@ -21,17 +21,17 @@ type CreateExternalAccountKeyResponse struct { // CreateExternalAccountKey creates a new External Account Binding key func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { - var eakRequest = new(CreateExternalAccountKeyRequest) - if err := api.ReadJSON(r.Body, eakRequest); err != nil { // TODO: rewrite into protobuf json (likely) + var body CreateExternalAccountKeyRequest + if err := api.ReadJSON(r.Body, &body); err != nil { // TODO: rewrite into protobuf json (likely) api.WriteError(w, err) return } // TODO: Validate input - eak, err := h.db.CreateExternalAccountKey(r.Context(), eakRequest.Name) + eak, err := h.db.CreateExternalAccountKey(r.Context(), body.Name) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", eakRequest.Name)) + api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) return } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 561db687..ba72581d 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -40,5 +40,5 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) // External Account Binding Keys - r.MethodFunc("POST", "/eak", h.CreateExternalAccountKey) // TODO: authnz + r.MethodFunc("POST", "/eak", authnz(h.CreateExternalAccountKey)) } diff --git a/ca/adminClient.go b/ca/adminClient.go index 2f3d4b5d..4aa03db2 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -12,6 +12,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" adminAPI "github.com/smallstep/certificates/authority/admin/api" "github.com/smallstep/certificates/authority/provisioner" @@ -558,6 +559,43 @@ retry: return nil } +// CreateExternalAccountKey performs the POST /admin/eak request to the CA. +func (c *AdminClient) CreateExternalAccountKey(eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*adminAPI.CreateExternalAccountKeyResponse, error) { + var retried bool + //body, err := protojson.Marshal(req) + body, err := json.Marshal(eakRequest) + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "eak")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, errors.Wrapf(err, "error generating admin token") + } + req, err := http.NewRequest("POST", u.String(), bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "create POST %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 POST %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var eakResp = new(adminAPI.CreateExternalAccountKeyResponse) + if err := api.ReadJSON(resp.Body, &eakResp); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return eakResp, nil +} + func readAdminError(r io.ReadCloser) error { defer r.Close() adminErr := new(admin.Error) From c6bfc6eac249443b46041d480a3aada809755fe9 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 22 Jul 2021 23:48:41 +0200 Subject: [PATCH 07/47] Fix PR comments --- acme/account.go | 6 ++- acme/api/account.go | 24 +++------ acme/api/handler.go | 8 +-- acme/api/middleware.go | 14 ++++++ authority/admin/api/eak.go | 2 +- authority/admin/api/handler.go | 16 +++--- authority/admin/db.go | 15 +----- authority/admin/db/nosql/admin_test.go | 19 ++++---- authority/admin/db/nosql/eak.go | 51 -------------------- authority/admin/db/nosql/nosql.go | 7 ++- authority/admin/db/nosql/provisioner_test.go | 19 ++++---- ca/ca.go | 2 +- 12 files changed, 62 insertions(+), 121 deletions(-) delete mode 100644 authority/admin/db/nosql/eak.go diff --git a/acme/account.go b/acme/account.go index 0113561f..330d2839 100644 --- a/acme/account.go +++ b/acme/account.go @@ -52,8 +52,12 @@ type ExternalAccountKey struct { BoundAt time.Time `json:"boundAt,omitempty"` } +func (eak *ExternalAccountKey) AlreadyBound() bool { + return !eak.BoundAt.IsZero() +} + func (eak *ExternalAccountKey) BindTo(account *Account) { eak.AccountID = account.ID eak.BoundAt = time.Now() - eak.KeyBytes = []byte{} // TODO: ensure that single use keys are OK + eak.KeyBytes = []byte{} // clearing the key bytes; can only be used once } diff --git a/acme/api/account.go b/acme/api/account.go index 9a48574a..8e9e9b11 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -8,7 +8,6 @@ import ( "github.com/go-chi/chi" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" - "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" squarejose "gopkg.in/square/go-jose.v2" @@ -125,7 +124,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { api.WriteError(w, acme.WrapErrorISE(err, "error creating account")) return } - if eak != nil { // means that we have a (valid) External Account Binding key that should be used + if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response eak.BindTo(acc) if err := h.db.UpdateExternalAccountKey(ctx, eak); err != nil { api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key")) @@ -134,7 +133,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { acc.ExternalAccountBinding = nar.ExternalAccountBinding } } else { - // Account exists // + // Account exists httpStatus = http.StatusOK } @@ -227,19 +226,12 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { // validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { - - prov, err := provisionerFromContext(ctx) + acmeProv, err := acmeProvisionerFromContext(ctx) if err != nil { - return nil, err - } - - acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner - if !ok || acmeProv == nil { - return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner") + return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context") } - shouldSkipAccountBindingValidation := !acmeProv.RequireEAB - if shouldSkipAccountBindingValidation { + if !acmeProv.RequireEAB { return nil, nil } @@ -249,7 +241,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) if err != nil { - return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding") + return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into JSON") } eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) @@ -266,8 +258,8 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.WrapErrorISE(err, "error retrieving external account key") } - if !externalAccountKey.BoundAt.IsZero() { // TODO: ensure that single use keys are OK - return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already used", keyID) + 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) diff --git a/acme/api/handler.go b/acme/api/handler.go index 4519bd38..e740cc1e 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -153,18 +153,12 @@ func (d *Directory) ToLog() (interface{}, error) { // for client configuration. func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - prov, err := provisionerFromContext(ctx) + acmeProv, err := acmeProvisionerFromContext(ctx) if err != nil { api.WriteError(w, err) return } - acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner - if !ok || acmeProv == nil { - api.WriteError(w, acme.NewErrorISE("provisioner in context is not an ACME provisioner")) - return - } - api.JSON(w, &Directory{ NewNonce: h.linker.GetLink(ctx, NewNonceLinkType), NewAccount: h.linker.GetLink(ctx, NewAccountLinkType), diff --git a/acme/api/middleware.go b/acme/api/middleware.go index b2244dd7..0fb38d1e 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -471,6 +471,20 @@ func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) { return pval, nil } +// acmeProvisionerFromContext searches the context for an ACME provisioner. Returns +// pointer to an ACME provisioner or an error. +func acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) { + prov, err := provisionerFromContext(ctx) + if err != nil { + return nil, err + } + acmeProv, ok := prov.(*provisioner.ACME) + if !ok || acmeProv == nil { + return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner") + } + return acmeProv, nil +} + // payloadFromContext searches the context for a payload. Returns the payload // or an error. func payloadFromContext(ctx context.Context) (*payloadInfo, error) { diff --git a/authority/admin/api/eak.go b/authority/admin/api/eak.go index 6f785f7a..8ea83309 100644 --- a/authority/admin/api/eak.go +++ b/authority/admin/api/eak.go @@ -29,7 +29,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques // TODO: Validate input - eak, err := h.db.CreateExternalAccountKey(r.Context(), body.Name) + eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Name) if err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) return diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index ba72581d..b20e29ab 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -1,6 +1,7 @@ package api import ( + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/admin" @@ -8,15 +9,18 @@ import ( // Handler is the ACME API request handler. type Handler struct { - db admin.DB - auth *authority.Authority + db admin.DB + auth *authority.Authority + acmeDB acme.DB } // NewHandler returns a new Authority Config Handler. -func NewHandler(auth *authority.Authority) api.RouterHandler { - h := &Handler{db: auth.GetAdminDatabase(), auth: auth} - - return h +func NewHandler(auth *authority.Authority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler { + return &Handler{ + db: adminDB, + auth: auth, + acmeDB: acmeDB, + } } // Route traffic and implement the Router interface. diff --git a/authority/admin/db.go b/authority/admin/db.go index 14520207..8a6339d9 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -7,8 +7,6 @@ import ( "github.com/pkg/errors" "go.step.sm/linkedca" - - "github.com/smallstep/certificates/authority/admin/eak" ) const ( @@ -56,7 +54,7 @@ func UnmarshalProvisionerDetails(typ linkedca.Provisioner_Type, data []byte) (*l return &linkedca.ProvisionerDetails{Data: v.Data}, nil } -// DB is the DB interface expected by the step-ca ACME API. +// DB is the DB interface expected by the step-ca Admin API. type DB interface { CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) @@ -69,8 +67,6 @@ type DB interface { GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error DeleteAdmin(ctx context.Context, id string) error - - CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) } // MockDB is an implementation of the DB interface that should only be used as @@ -88,8 +84,6 @@ type MockDB struct { MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error MockDeleteAdmin func(ctx context.Context, id string) error - MockCreateExternalAccountKey func(ctx context.Context, name string) (*eak.ExternalAccountKey, error) - MockError error MockRet1 interface{} } @@ -183,10 +177,3 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error { } return m.MockError } - -func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) { - if m.MockCreateExternalAccountKey != nil { - return m.MockCreateExternalAccountKey(ctx, name) - } - return m.MockRet1.(*eak.ExternalAccountKey), m.MockError -} diff --git a/authority/admin/db/nosql/admin_test.go b/authority/admin/db/nosql/admin_test.go index 092d72db..013cb489 100644 --- a/authority/admin/db/nosql/admin_test.go +++ b/authority/admin/db/nosql/admin_test.go @@ -11,7 +11,6 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" - "github.com/smallstep/nosql/database" nosqldb "github.com/smallstep/nosql/database" "go.step.sm/linkedca" "google.golang.org/protobuf/types/known/timestamppb" @@ -996,7 +995,7 @@ func TestDB_GetAdmins(t *testing.T) { "fail/db.List-error": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return nil, errors.New("force") @@ -1006,14 +1005,14 @@ func TestDB_GetAdmins(t *testing.T) { } }, "fail/unmarshal-error": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: adminsTable, Key: []byte("foo"), Value: foob}, {Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("zap"), Value: []byte("zap")}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil @@ -1023,10 +1022,10 @@ func TestDB_GetAdmins(t *testing.T) { } }, "ok/none": func(t *testing.T) test { - ret := []*database.Entry{} + ret := []*nosqldb.Entry{} return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil @@ -1038,13 +1037,13 @@ func TestDB_GetAdmins(t *testing.T) { } }, "ok/only-invalid": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("baz"), Value: bazb}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil @@ -1056,7 +1055,7 @@ func TestDB_GetAdmins(t *testing.T) { } }, "ok": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: adminsTable, Key: []byte("foo"), Value: foob}, {Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("baz"), Value: bazb}, @@ -1064,7 +1063,7 @@ func TestDB_GetAdmins(t *testing.T) { } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil diff --git a/authority/admin/db/nosql/eak.go b/authority/admin/db/nosql/eak.go deleted file mode 100644 index a3a3d96d..00000000 --- a/authority/admin/db/nosql/eak.go +++ /dev/null @@ -1,51 +0,0 @@ -package nosql - -import ( - "context" - "crypto/rand" - "time" - - "github.com/smallstep/certificates/authority/admin/eak" -) - -type dbExternalAccountKey struct { - ID string `json:"id"` - Name string `json:"name"` - AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key,omitempty"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt"` -} - -// CreateExternalAccountKey creates a new External Account Binding key -func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) { - keyID, err := randID() - if err != nil { - return nil, err - } - - random := make([]byte, 32) - _, err = rand.Read(random) - if err != nil { - return nil, err - } - - dbeak := &dbExternalAccountKey{ - ID: keyID, - Name: name, - KeyBytes: random, - CreatedAt: clock.Now(), - } - - if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { - return nil, err - } - return &eak.ExternalAccountKey{ - ID: dbeak.ID, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, - }, nil -} diff --git a/authority/admin/db/nosql/nosql.go b/authority/admin/db/nosql/nosql.go index 8019cbdf..18599b02 100644 --- a/authority/admin/db/nosql/nosql.go +++ b/authority/admin/db/nosql/nosql.go @@ -11,9 +11,8 @@ import ( ) var ( - adminsTable = []byte("admins") - provisionersTable = []byte("provisioners") - externalAccountKeyTable = []byte("acme_external_account_keys") + adminsTable = []byte("admins") + provisionersTable = []byte("provisioners") ) // DB is a struct that implements the AdminDB interface. @@ -24,7 +23,7 @@ type DB struct { // New configures and returns a new Authority DB backend implemented using a nosql DB. func New(db nosqlDB.DB, authorityID string) (*DB, error) { - tables := [][]byte{adminsTable, provisionersTable, externalAccountKeyTable} + tables := [][]byte{adminsTable, provisionersTable} for _, b := range tables { if err := db.CreateTable(b); err != nil { return nil, errors.Wrapf(err, "error creating table %s", diff --git a/authority/admin/db/nosql/provisioner_test.go b/authority/admin/db/nosql/provisioner_test.go index 95811f26..9759f5d8 100644 --- a/authority/admin/db/nosql/provisioner_test.go +++ b/authority/admin/db/nosql/provisioner_test.go @@ -11,7 +11,6 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" - "github.com/smallstep/nosql/database" nosqldb "github.com/smallstep/nosql/database" "go.step.sm/linkedca" ) @@ -746,7 +745,7 @@ func TestDB_GetProvisioners(t *testing.T) { "fail/db.List-error": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return nil, errors.New("force") @@ -756,14 +755,14 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "fail/unmarshal-error": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: provisionersTable, Key: []byte("foo"), Value: foob}, {Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("zap"), Value: []byte("zap")}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil @@ -773,10 +772,10 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "ok/none": func(t *testing.T) test { - ret := []*database.Entry{} + ret := []*nosqldb.Entry{} return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil @@ -788,13 +787,13 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "ok/only-invalid": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("baz"), Value: bazb}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil @@ -806,7 +805,7 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "ok": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: provisionersTable, Key: []byte("foo"), Value: foob}, {Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("baz"), Value: bazb}, @@ -814,7 +813,7 @@ func TestDB_GetProvisioners(t *testing.T) { } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil diff --git a/ca/ca.go b/ca/ca.go index 4551286b..059101dc 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -182,7 +182,7 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { if config.AuthorityConfig.EnableAdmin { adminDB := auth.GetAdminDatabase() if adminDB != nil { - adminHandler := adminAPI.NewHandler(auth) + adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB) mux.Route("/admin", func(r chi.Router) { adminHandler.Route(r) }) From c6a4c4ecba3d59445fce0b020b64d21cc4364c76 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 23 Jul 2021 15:16:11 +0200 Subject: [PATCH 08/47] Change ACME EAB endpoint --- authority/admin/api/eak.go | 45 ----------------------- authority/admin/api/handler.go | 5 +-- authority/admin/eak/eak.go | 12 ------- ca/adminClient.go | 65 ++++++++++++++++++++++++++++++++-- 4 files changed, 65 insertions(+), 62 deletions(-) delete mode 100644 authority/admin/api/eak.go delete mode 100644 authority/admin/eak/eak.go diff --git a/authority/admin/api/eak.go b/authority/admin/api/eak.go deleted file mode 100644 index 8ea83309..00000000 --- a/authority/admin/api/eak.go +++ /dev/null @@ -1,45 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/smallstep/certificates/api" - "github.com/smallstep/certificates/authority/admin" -) - -// CreateExternalAccountKeyRequest is the type for POST /admin/eak requests -type CreateExternalAccountKeyRequest struct { - Name string `json:"name"` -} - -// CreateExternalAccountKeyResponse is the type for POST /admin/eak responses -type CreateExternalAccountKeyResponse struct { - KeyID string `json:"keyID"` - Name string `json:"name"` - Key []byte `json:"key"` -} - -// CreateExternalAccountKey creates a new External Account Binding key -func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { - var body CreateExternalAccountKeyRequest - if err := api.ReadJSON(r.Body, &body); err != nil { // TODO: rewrite into protobuf json (likely) - api.WriteError(w, err) - return - } - - // TODO: Validate input - - eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Name) - if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) - return - } - - eakResponse := CreateExternalAccountKeyResponse{ - KeyID: eak.ID, - Name: eak.Name, - Key: eak.KeyBytes, - } - - api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely) -} diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index b20e29ab..0e49dfd9 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -43,6 +43,7 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin)) r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) - // External Account Binding Keys - r.MethodFunc("POST", "/eak", authnz(h.CreateExternalAccountKey)) + // ACME External Account Binding Keys + r.MethodFunc("GET", "/acme/eab", authnz(h.GetExternalAccountKeys)) + r.MethodFunc("POST", "/acme/eab", authnz(h.CreateExternalAccountKey)) } diff --git a/authority/admin/eak/eak.go b/authority/admin/eak/eak.go deleted file mode 100644 index b2eaa157..00000000 --- a/authority/admin/eak/eak.go +++ /dev/null @@ -1,12 +0,0 @@ -package eak - -import "time" - -type ExternalAccountKey struct { - ID string `json:"id"` - Name string `json:"name"` - AccountID string `json:"-"` - KeyBytes []byte `json:"-"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt,omitempty"` -} diff --git a/ca/adminClient.go b/ca/adminClient.go index 4aa03db2..7f24efa9 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -559,15 +559,54 @@ retry: return nil } -// CreateExternalAccountKey performs the POST /admin/eak request to the CA. +// GetExternalAccountKeysPaginate returns a page from the the GET /admin/acme/eab request to the CA. +func (c *AdminClient) GetExternalAccountKeysPaginate(opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { + var retried bool + o := new(adminOptions) + if err := o.apply(opts); err != nil { + return nil, err + } + u := c.endpoint.ResolveReference(&url.URL{ + Path: "/admin/acme/eab", + 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(), nil) + 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(GetExternalAccountKeysResponse) + // if err := readJSON(resp.Body, body); err != nil { + // return nil, errors.Wrapf(err, "error reading %s", u) + // } + // return body, nil + return nil, nil // TODO: fix correctly +} + +// CreateExternalAccountKey performs the POST /admin/acme/eab request to the CA. func (c *AdminClient) CreateExternalAccountKey(eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*adminAPI.CreateExternalAccountKeyResponse, error) { var retried bool - //body, err := protojson.Marshal(req) body, err := json.Marshal(eakRequest) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } - u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "eak")}) + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab")}) tok, err := c.generateAdminToken(u.Path) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") @@ -596,7 +635,27 @@ retry: return eakResp, nil } +// GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. +func (c *AdminClient) GetExternalAccountKeys(opts ...AdminOption) ([]*adminAPI.CreateExternalAccountKeyResponse, error) { + var ( + cursor = "" + eaks = []*adminAPI.CreateExternalAccountKeyResponse{} + ) + for { + resp, err := c.GetExternalAccountKeysPaginate(WithAdminCursor(cursor), WithAdminLimit(100)) + if err != nil { + return nil, err + } + eaks = append(eaks, resp.EAKs...) + if resp.NextCursor == "" { + return eaks, nil + } + cursor = resp.NextCursor + } +} + 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() adminErr := new(admin.Error) if err := json.NewDecoder(r).Decode(adminErr); err != nil { From 7dad7038c398a0ab2b3b2f6537220f2682f1a146 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 23 Jul 2021 15:41:24 +0200 Subject: [PATCH 09/47] Fix missing ACME EAB API endpoints --- authority/admin/api/acme.go | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 authority/admin/api/acme.go diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go new file mode 100644 index 00000000..f193bcb5 --- /dev/null +++ b/authority/admin/api/acme.go @@ -0,0 +1,71 @@ +package api + +import ( + "net/http" + + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" +) + +// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests +type CreateExternalAccountKeyRequest struct { + Name string `json:"name"` +} + +// CreateExternalAccountKeyResponse is the type for POST /admin/acme/eab responses +type CreateExternalAccountKeyResponse struct { + KeyID string `json:"keyID"` + Name string `json:"name"` + Key []byte `json:"key"` +} + +// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses +type GetExternalAccountKeysResponse struct { + EAKs []*CreateExternalAccountKeyResponse `json:"eaks"` + NextCursor string `json:"nextCursor"` +} + +// CreateExternalAccountKey creates a new External Account Binding key +func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { + var body CreateExternalAccountKeyRequest + if err := api.ReadJSON(r.Body, &body); err != nil { // TODO: rewrite into protobuf json (likely) + api.WriteError(w, err) + return + } + + // TODO: Validate input + + eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Name) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) + return + } + + eakResponse := CreateExternalAccountKeyResponse{ + KeyID: eak.ID, + Name: eak.Name, + Key: eak.KeyBytes, + } + + api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely) +} + +// GetExternalAccountKeys returns a segment of ACME EAB Keys. +func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { + // cursor, limit, err := api.ParseCursor(r) + // if err != nil { + // api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, + // "error parsing cursor and limit from query params")) + // return + // } + + // eaks, nextCursor, err := h.acmeDB.GetExternalAccountKeys(cursor, limit) + // if err != nil { + // api.WriteError(w, admin.WrapErrorISE(err, "error retrieving paginated admins")) + // return + // } + // api.JSON(w, &GetExternalAccountKeysResponse{ + // EAKs: eaks, + // NextCursor: nextCursor, + // }) +} From 71b3f65df12d532b26be8532d3264931da8b47bb Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 7 Aug 2021 01:33:08 +0200 Subject: [PATCH 10/47] Add processing of RequireEAB through Linked CA --- authority/provisioners.go | 13 +++++++------ docs/provisioners.md | 4 ++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/authority/provisioners.go b/authority/provisioners.go index d2581e76..0117e037 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -510,12 +510,13 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, case *linkedca.ProvisionerDetails_ACME: cfg := d.ACME return &provisioner.ACME{ - ID: p.Id, - Type: p.Type.String(), - Name: p.Name, - ForceCN: cfg.ForceCn, - Claims: claims, - Options: options, + ID: p.Id, + Type: p.Type.String(), + Name: p.Name, + ForceCN: cfg.ForceCn, + RequireEAB: cfg.RequireEab, + Claims: claims, + Options: options, }, nil case *linkedca.ProvisionerDetails_OIDC: cfg := d.OIDC diff --git a/docs/provisioners.md b/docs/provisioners.md index 7ee9af50..e3770023 100644 --- a/docs/provisioners.md +++ b/docs/provisioners.md @@ -346,6 +346,7 @@ Below is an example of an ACME provisioner in the `ca.json`: "type": "ACME", "name": "my-acme-provisioner", "forceCN": true, + "requireEAB": false, "claims": { "maxTLSCertDuration": "8h", "defaultTLSCertDuration": "2h", @@ -361,6 +362,9 @@ Below is an example of an ACME provisioner in the `ca.json`: * `forceCN` (optional): force one of the SANs to become the Common Name, if a common name is not provided. +* `requireEAB` (optional): require clients to provide External Account Binding + credentials when creating an ACME Account. + * `claims` (optional): overwrites the default claims set in the authority, see the [top](#provisioners) section for all the options. From 492256f2d78d037b536b37579819eef44504bfa9 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 9 Aug 2021 10:26:31 +0200 Subject: [PATCH 11/47] Add first test cases for EAB and make provisioner unique per EAB Before this commit, EAB keys could be used CA-wide, meaning that an EAB credential could be used at any ACME provisioner. This commit changes that behavior, so that EAB credentials are now intended to be used with a specific ACME provisioner. I think that makes sense, because from the perspective of an ACME client the provisioner is like a distinct CA. Besides that this commit also includes the first tests for EAB. The logic for creating the EAB JWS as a client has been taken from github.com/mholt/acmez. This logic may be moved or otherwise sourced (i.e. from a vendor) as soon as the step client also (needs to) support(s) EAB with ACME. --- acme/account.go | 13 +- acme/api/account.go | 17 +- acme/api/account_test.go | 338 ++++++++++++++++++++++++++++++++++++ acme/db.go | 24 +-- acme/db/nosql/account.go | 75 ++++---- authority/admin/api/acme.go | 19 +- 6 files changed, 426 insertions(+), 60 deletions(-) diff --git a/acme/account.go b/acme/account.go index 330d2839..042cbdd3 100644 --- a/acme/account.go +++ b/acme/account.go @@ -44,12 +44,13 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { } type ExternalAccountKey struct { - ID string `json:"id"` - Name string `json:"name"` - AccountID string `json:"-"` - KeyBytes []byte `json:"-"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt,omitempty"` + ID string `json:"id"` + ProvisionerName string `json:"provisioner_name"` + Name string `json:"name"` + AccountID string `json:"-"` + KeyBytes []byte `json:"-"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt,omitempty"` } func (eak *ExternalAccountKey) AlreadyBound() bool { diff --git a/acme/api/account.go b/acme/api/account.go index 8e9e9b11..6484609f 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -93,6 +93,12 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } + prov, err := acmeProvisionerFromContext(ctx) + if err != nil { + api.WriteError(w, err) + return + } + httpStatus := http.StatusCreated acc, err := accountFromContext(r.Context()) if err != nil { @@ -126,7 +132,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { } if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response eak.BindTo(acc) - if err := h.db.UpdateExternalAccountKey(ctx, eak); err != nil { + if err := h.db.UpdateExternalAccountKey(ctx, prov.Name, eak); err != nil { api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key")) return } @@ -224,7 +230,7 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) { logOrdersByAccount(w, orders) } -// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account +// 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 { @@ -253,8 +259,11 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc // TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration keyID := eabJWS.Signatures[0].Protected.KeyID - externalAccountKey, err := h.db.GetExternalAccountKey(ctx, keyID) + externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.Name, keyID) if err != nil { + if _, ok := err.(*acme.Error); ok { + return nil, err + } return nil, acme.WrapErrorISE(err, "error retrieving external account key") } @@ -285,6 +294,8 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return externalAccountKey, nil } +// keysAreEqual performs an equality check on two JWKs by comparing +// the (base64 encoding) of the Key IDs. func keysAreEqual(x, y *squarejose.JSONWebKey) bool { if x == nil || y == nil { return false diff --git a/acme/api/account_test.go b/acme/api/account_test.go index fe0f5392..e68999e2 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -3,11 +3,19 @@ package api import ( "bytes" "context" + "crypto" + "crypto/ecdsa" + "crypto/hmac" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" "io/ioutil" + "math/big" "net/http/httptest" "net/url" + "reflect" "testing" "time" @@ -16,6 +24,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" + squarejose "gopkg.in/square/go-jose.v2" ) var ( @@ -40,6 +49,136 @@ func newProv() acme.Provisioner { return p } +func newACMEProv(t *testing.T) *provisioner.ACME { + p := newProv() + a, ok := p.(*provisioner.ACME) + if !ok { + t.Fatal("not a valid ACME provisioner") + } + return a +} + +var errUnsupportedKey = fmt.Errorf("unknown key type; only RSA and ECDSA are supported") + +// keyID is the account identity provided by a CA during registration. +type keyID string + +// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID. +// See jwsEncodeJSON for details. +const noKeyID = keyID("") + +// jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4. +// Implementation taken from github.com/mholt/acmez +func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url string) ([]byte, error) { + // §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm" + alg, sha := "HS256", crypto.SHA256 + + // §7.3.4: "The 'nonce' field MUST NOT be present" + phead, err := jwsHead(alg, "", url, kid, nil) + if err != nil { + return nil, err + } + + encodedKey, err := jwkEncode(accountKey) + if err != nil { + return nil, err + } + + payload := base64.RawURLEncoding.EncodeToString([]byte(encodedKey)) + + payloadToSign := []byte(phead + "." + payload) + + h := hmac.New(sha256.New, hmacKey) + h.Write(payloadToSign) + sig := h.Sum(nil) + + return jwsFinal(sha, sig, phead, payload) +} + +// jwsHead constructs the protected JWS header for the given fields. +// Since jwk and kid are mutually-exclusive, the jwk will be encoded +// only if kid is empty. If nonce is empty, it will not be encoded. +// Implementation taken from github.com/mholt/acmez +func jwsHead(alg, nonce, url string, kid keyID, key crypto.Signer) (string, error) { + phead := fmt.Sprintf(`{"alg":%q`, alg) + if kid == noKeyID { + jwk, err := jwkEncode(key.Public()) + if err != nil { + return "", err + } + phead += fmt.Sprintf(`,"jwk":%s`, jwk) + } else { + phead += fmt.Sprintf(`,"kid":%q`, kid) + } + if nonce != "" { + phead += fmt.Sprintf(`,"nonce":%q`, nonce) + } + phead += fmt.Sprintf(`,"url":%q}`, url) + phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) + return phead, nil +} + +// jwkEncode encodes public part of an RSA or ECDSA key into a JWK. +// The result is also suitable for creating a JWK thumbprint. +// https://tools.ietf.org/html/rfc7517 +// Implementation taken from github.com/mholt/acmez +func jwkEncode(pub crypto.PublicKey) (string, error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + n := pub.N + e := big.NewInt(int64(pub.E)) + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, + base64.RawURLEncoding.EncodeToString(e.Bytes()), + base64.RawURLEncoding.EncodeToString(n.Bytes()), + ), nil + case *ecdsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.2.1 + p := pub.Curve.Params() + n := p.BitSize / 8 + if p.BitSize%8 != 0 { + n++ + } + x := pub.X.Bytes() + if n > len(x) { + x = append(make([]byte, n-len(x)), x...) + } + y := pub.Y.Bytes() + if n > len(y) { + y = append(make([]byte, n-len(y)), y...) + } + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, + p.Name, + base64.RawURLEncoding.EncodeToString(x), + base64.RawURLEncoding.EncodeToString(y), + ), nil + } + return "", errUnsupportedKey +} + +// jwsFinal constructs the final JWS object. +// Implementation taken from github.com/mholt/acmez +func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) { + enc := struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` + }{ + Protected: phead, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(sig), + } + result, err := json.Marshal(&enc) + if err != nil { + return nil, err + } + return result, nil +} + func TestNewAccountRequest_Validate(t *testing.T) { type test struct { nar *NewAccountRequest @@ -377,6 +516,27 @@ func TestHandler_NewAccount(t *testing.T) { err: acme.NewErrorISE("jwk expected in request context"), } }, + "fail/new-account-no-eab-provided": func(t *testing.T) test { + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: nil, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + 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, prov) + return test{ + ctx: ctx, + statusCode: 400, + err: acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided"), + } + }, "fail/db.CreateAccount-error": func(t *testing.T) test { nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -456,6 +616,94 @@ func TestHandler_NewAccount(t *testing.T) { statusCode: 200, } }, + "ok/new-account-no-eab-required": func(t *testing.T) test { + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: struct{}{}, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = false + 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, prov) + return test{ + db: &acme.MockDB{ + MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" + assert.Equals(t, acc.Contact, nar.Contact) + assert.Equals(t, acc.Key, jwk) + return nil + }, + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), + }, + ctx: ctx, + statusCode: 201, + } + }, + "ok/new-account-with-eab": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + mappedEAB := make(map[string]interface{}) + err = json.Unmarshal(eabJWS, &mappedEAB) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: mappedEAB, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + 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, prov) + return test{ + db: &acme.MockDB{ + MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" + assert.Equals(t, acc.Contact, nar.Contact) + assert.Equals(t, acc.Key, jwk) + return nil + }, + MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerName: escProvName, + Name: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { + return nil + }, + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), + ExternalAccountBinding: mappedEAB, + }, + ctx: ctx, + statusCode: 201, + } + }, } for name, run := range tests { tc := run(t) @@ -694,3 +942,93 @@ 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) + type args struct { + x *squarejose.JSONWebKey + y *squarejose.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, + }, + } + 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) { + type fields struct { + db acme.DB + backdate provisioner.Duration + ca acme.CertificateAuthority + linker Linker + validateChallengeOptions *acme.ValidateChallengeOptions + } + type args struct { + ctx context.Context + nar *NewAccountRequest + } + tests := []struct { + name string + fields fields + args args + want *acme.ExternalAccountKey + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &Handler{ + db: tt.fields.db, + backdate: tt.fields.backdate, + ca: tt.fields.ca, + linker: tt.fields.linker, + validateChallengeOptions: tt.fields.validateChallengeOptions, + } + got, err := h.validateExternalAccountBinding(tt.args.ctx, tt.args.nar) + if (err != nil) != tt.wantErr { + t.Errorf("Handler.validateExternalAccountBinding() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Handler.validateExternalAccountBinding() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/acme/db.go b/acme/db.go index b80e8f81..46e3fc97 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,9 +19,9 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error - CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) - GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) - UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error + CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) + GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error CreateNonce(ctx context.Context) (Nonce, error) DeleteNonce(ctx context.Context, nonce Nonce) error @@ -51,9 +51,9 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error - MockCreateExternalAccountKey func(ctx context.Context, name string) (*ExternalAccountKey, error) - MockGetExternalAccountKey func(ctx context.Context, keyID string) (*ExternalAccountKey, error) - MockUpdateExternalAccountKey func(ctx context.Context, eak *ExternalAccountKey) error + MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) + MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockCreateNonce func(ctx context.Context) (Nonce, error) MockDeleteNonce func(ctx context.Context, nonce Nonce) error @@ -119,9 +119,9 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error { } // CreateExternalAccountKey mock -func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error) { +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) { if m.MockCreateExternalAccountKey != nil { - return m.MockCreateExternalAccountKey(ctx, name) + return m.MockCreateExternalAccountKey(ctx, provisionerName, name) } else if m.MockError != nil { return nil, m.MockError } @@ -129,18 +129,18 @@ func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*Ex } // GetExternalAccountKey mock -func (m *MockDB) GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) { if m.MockGetExternalAccountKey != nil { - return m.MockGetExternalAccountKey(ctx, keyID) + return m.MockGetExternalAccountKey(ctx, provisionerName, keyID) } else if m.MockError != nil { return nil, m.MockError } return m.MockRet1.(*ExternalAccountKey), m.MockError } -func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error { +func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error { if m.MockUpdateExternalAccountKey != nil { - return m.MockUpdateExternalAccountKey(ctx, eak) + return m.MockUpdateExternalAccountKey(ctx, provisionerName, eak) } else if m.MockError != nil { return m.MockError } diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index e38ff425..dc909dde 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -28,12 +28,13 @@ func (dba *dbAccount) clone() *dbAccount { } type dbExternalAccountKey struct { - ID string `json:"id"` - Name string `json:"name"` - AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt"` + ID string `json:"id"` + ProvisionerName string `json:"provisioner_name"` + Name string `json:"name"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` } func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) { @@ -164,7 +165,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { } // CreateExternalAccountKey creates a new External Account Binding key with a name -func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme.ExternalAccountKey, error) { +func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*acme.ExternalAccountKey, error) { keyID, err := randID() if err != nil { return nil, err @@ -177,55 +178,67 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*acme. } dbeak := &dbExternalAccountKey{ - ID: keyID, - Name: name, - KeyBytes: random, - CreatedAt: clock.Now(), + ID: keyID, + ProvisionerName: provisionerName, + Name: name, + KeyBytes: random, + CreatedAt: clock.Now(), } if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { return nil, err } return &acme.ExternalAccountKey{ - ID: dbeak.ID, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + ProvisionerName: dbeak.ProvisionerName, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, nil } // GetExternalAccountKey retrieves an External Account Binding key by KeyID -func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return nil, err } + if dbeak.ProvisionerName != provisionerName { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + } + return &acme.ExternalAccountKey{ - ID: dbeak.ID, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + ProvisionerName: dbeak.ProvisionerName, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, nil } -func (db *DB) UpdateExternalAccountKey(ctx context.Context, eak *acme.ExternalAccountKey) error { +func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { old, err := db.getDBExternalAccountKey(ctx, eak.ID) if err != nil { return err } + if old.ProvisionerName != provisionerName { + return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + } + nu := dbExternalAccountKey{ - ID: eak.ID, - Name: eak.Name, - AccountID: eak.AccountID, - KeyBytes: eak.KeyBytes, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, + ID: eak.ID, + ProvisionerName: eak.ProvisionerName, + Name: eak.Name, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, } return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index f193bcb5..36fc10fe 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -9,14 +9,16 @@ import ( // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests type CreateExternalAccountKeyRequest struct { - Name string `json:"name"` + ProvisionerName string `json:"provisioner"` + Name string `json:"name"` } // CreateExternalAccountKeyResponse is the type for POST /admin/acme/eab responses type CreateExternalAccountKeyResponse struct { - KeyID string `json:"keyID"` - Name string `json:"name"` - Key []byte `json:"key"` + ProvisionerName string `json:"provisioner"` + KeyID string `json:"keyID"` + Name string `json:"name"` + Key []byte `json:"key"` } // GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses @@ -35,16 +37,17 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques // TODO: Validate input - eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Name) + eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.ProvisionerName, body.Name) if err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) return } eakResponse := CreateExternalAccountKeyResponse{ - KeyID: eak.ID, - Name: eak.Name, - Key: eak.KeyBytes, + ProvisionerName: eak.ProvisionerName, + KeyID: eak.ID, + Name: eak.Name, + Key: eak.KeyBytes, } api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely) From f31ca4f6a4de947ac0bfbe5b6d7789e858ef3ff0 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 10 Aug 2021 12:39:11 +0200 Subject: [PATCH 12/47] Add tests for validateExternalAccountBinding --- acme/account.go | 2 +- acme/api/account.go | 18 +- acme/api/account_test.go | 375 ++++++++++++++++++++++++++++++---- authority/provisioner/acme.go | 12 +- go.mod | 2 + 5 files changed, 361 insertions(+), 48 deletions(-) diff --git a/acme/account.go b/acme/account.go index 042cbdd3..e9d3ac7f 100644 --- a/acme/account.go +++ b/acme/account.go @@ -45,7 +45,7 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { type ExternalAccountKey struct { ID string `json:"id"` - ProvisionerName string `json:"provisioner_name"` + ProvisionerName string `json:"provisionerName"` Name string `json:"name"` AccountID string `json:"-"` KeyBytes []byte `json:"-"` diff --git a/acme/api/account.go b/acme/api/account.go index 6484609f..bf8fdfe5 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -13,12 +13,19 @@ import ( squarejose "gopkg.in/square/go-jose.v2" ) +// 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"` - OnlyReturnExisting bool `json:"onlyReturnExisting"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` - ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"` + Contact []string `json:"contact"` + OnlyReturnExisting bool `json:"onlyReturnExisting"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + ExternalAccountBinding *ExternalAccountBinding `json:"externalAccountBinding,omitempty"` } func validateContacts(cs []string) error { @@ -245,6 +252,9 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") } + // TODO: extract the EAB in a similar manner as JWS, JWK, payload, etc? That would probably move a lot/all of + // the logic of this function into the middleware. Should not be too hard, because the middleware does know + // about the handler and thus about its dependencies. eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) if err != nil { return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into JSON") diff --git a/acme/api/account_test.go b/acme/api/account_test.go index e68999e2..f6a368d8 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -15,11 +15,11 @@ import ( "math/big" "net/http/httptest" "net/url" - "reflect" "testing" "time" "github.com/go-chi/chi" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" @@ -617,14 +617,19 @@ func TestHandler_NewAccount(t *testing.T) { } }, "ok/new-account-no-eab-required": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, - ExternalAccountBinding: struct{}{}, + ExternalAccountBinding: eab, } b, err := json.Marshal(nar) assert.FatalError(t, err) - jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = false ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) @@ -656,12 +661,12 @@ func TestHandler_NewAccount(t *testing.T) { assert.FatalError(t, err) eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) assert.FatalError(t, err) - mappedEAB := make(map[string]interface{}) - err = json.Unmarshal(eabJWS, &mappedEAB) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, - ExternalAccountBinding: mappedEAB, + ExternalAccountBinding: eab, } b, err := json.Marshal(nar) assert.FatalError(t, err) @@ -698,7 +703,7 @@ func TestHandler_NewAccount(t *testing.T) { Status: acme.StatusValid, Contact: []string{"foo", "bar"}, OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), - ExternalAccountBinding: mappedEAB, + ExternalAccountBinding: eab, }, ctx: ctx, statusCode: 201, @@ -948,6 +953,9 @@ func Test_keysAreEqual(t *testing.T) { 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 *squarejose.JSONWebKey y *squarejose.JSONWebKey @@ -981,6 +989,14 @@ func Test_keysAreEqual(t *testing.T) { }, 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) { @@ -992,42 +1008,323 @@ func Test_keysAreEqual(t *testing.T) { } func TestHandler_validateExternalAccountBinding(t *testing.T) { - type fields struct { - db acme.DB - backdate provisioner.Duration - ca acme.CertificateAuthority - linker Linker - validateChallengeOptions *acme.ValidateChallengeOptions - } - type args struct { + acmeProv := newACMEProv(t) + escProvName := url.PathEscape(acmeProv.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + type test struct { + db acme.DB ctx context.Context nar *NewAccountRequest + eak *acme.ExternalAccountKey + err *acme.Error } - tests := []struct { - name string - fields fields - args args - want *acme.ExternalAccountKey - wantErr bool - }{ - // TODO: Add test cases. + 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) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + 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) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + 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{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerName: escProvName, + Name: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: &acme.ExternalAccountKey{}, + err: nil, + } + }, + "fail/parse-eab-jose": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + eab.Payload = 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/retrieve-eab-key-db-failure": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + 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{ + 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/retrieve-eab-key-not-found": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "unknown-key-id", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + 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) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + return nil, acme.NewErrorISE("error retrieving external account key") + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error loading external account key unknown-key-id"), + } + }, + "fail/retrieve-eab-wrong-provisioner": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + 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{ + 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, "name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/eab-already-bound": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + 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) + createdAt := time.Now() + boundAt := time.Now().Add(1 * time.Second) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerName: escProvName, + Name: "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) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + 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{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerName: escProvName, + Name: "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) + eabJWS, err := jwsEncodeEAB(differentJWK.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + 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{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerName: escProvName, + Name: "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.ErrorMalformedType, "keys in jws and eab payload do not match"), + } + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { h := &Handler{ - db: tt.fields.db, - backdate: tt.fields.backdate, - ca: tt.fields.ca, - linker: tt.fields.linker, - validateChallengeOptions: tt.fields.validateChallengeOptions, - } - got, err := h.validateExternalAccountBinding(tt.args.ctx, tt.args.nar) - if (err != nil) != tt.wantErr { - t.Errorf("Handler.validateExternalAccountBinding() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Handler.validateExternalAccountBinding() = %v, want %v", got, tt.want) + db: tc.db, + } + got, err := h.validateExternalAccountBinding(tc.ctx, tc.nar) + wantErr := tc.err != nil + gotErr := err != nil + if wantErr != gotErr { + // fmt.Println(got) + // fmt.Println(fmt.Sprintf("%#+v", got)) + 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.Detail, tc.err.Detail) + assert.Equals(t, ae.Identifier, tc.err.Identifier) + assert.Equals(t, ae.Subproblems, tc.err.Subproblems) + + // fmt.Println(fmt.Sprintf("%#+v", ae)) + // fmt.Println(fmt.Sprintf("%#+v", tc.err)) + + //t.Fail() + } else { + if got == nil { + assert.Nil(t, tc.eak) + } else { + // TODO: equality check on certain fields? + assert.NotNil(t, tc.eak) + } + //assert.Equals(t, tc.eak, got) + //assert.NotNil(t, got) } }) } diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 9a0f1356..b4120d0f 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -13,10 +13,14 @@ import ( // provisioning flow. type ACME struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - ForceCN bool `json:"forceCN,omitempty"` + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + ForceCN bool `json:"forceCN,omitempty"` + // RequireEAB makes the provisioner require ACME EAB to be provided + // by clients when creating a new Account. If set to true, the provided + // EAB will be verified. If set to false and an EAB is provided, it is + // not verified. Defaults to false. RequireEAB bool `json:"requireEAB,omitempty"` Claims *Claims `json:"claims,omitempty"` Options *Options `json:"options,omitempty"` diff --git a/go.mod b/go.mod index 58228557..bbebec51 100644 --- a/go.mod +++ b/go.mod @@ -44,4 +44,6 @@ require ( //replace go.step.sm/cli-utils => ../cli-utils +//replace go.step.sm/linkedca => ../linkedca + replace go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 => github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568 From 1dba8698e3765864b4bf63612f57c2823ffe9b8f Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 27 Aug 2021 12:39:37 +0200 Subject: [PATCH 13/47] Use LinkedCA.EABKey type in ACME EAB API --- acme/api/account.go | 1 - authority/admin/api/acme.go | 35 +++++++++++++++++++++-------------- ca/adminClient.go | 13 ++++++------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index bf8fdfe5..fa5597e0 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -265,7 +265,6 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") } - // TODO: verify supported algorithms against the incoming alg (and corresponding settings)? // TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration keyID := eabJWS.Signatures[0].Protected.KeyID diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 36fc10fe..7772e435 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -5,6 +5,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" + "go.step.sm/linkedca" ) // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests @@ -13,29 +14,35 @@ type CreateExternalAccountKeyRequest struct { Name string `json:"name"` } -// CreateExternalAccountKeyResponse is the type for POST /admin/acme/eab responses -type CreateExternalAccountKeyResponse struct { - ProvisionerName string `json:"provisioner"` - KeyID string `json:"keyID"` - Name string `json:"name"` - Key []byte `json:"key"` +// Validate validates a new-admin request body. +func (r *CreateExternalAccountKeyRequest) Validate() error { + if r.ProvisionerName == "" { + return admin.NewError(admin.ErrorBadRequestType, "provisioner name cannot be empty") + } + if r.Name == "" { + return admin.NewError(admin.ErrorBadRequestType, "name / reference cannot be empty") + } + return nil } // GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses type GetExternalAccountKeysResponse struct { - EAKs []*CreateExternalAccountKeyResponse `json:"eaks"` - NextCursor string `json:"nextCursor"` + EAKs []*linkedca.EABKey `json:"eaks"` + NextCursor string `json:"nextCursor"` } // CreateExternalAccountKey creates a new External Account Binding key func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { var body CreateExternalAccountKeyRequest if err := api.ReadJSON(r.Body, &body); err != nil { // TODO: rewrite into protobuf json (likely) - api.WriteError(w, err) + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body")) return } - // TODO: Validate input + if err := body.Validate(); err != nil { + api.WriteError(w, err) + return + } eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.ProvisionerName, body.Name) if err != nil { @@ -43,14 +50,14 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques return } - eakResponse := CreateExternalAccountKeyResponse{ + response := &linkedca.EABKey{ + EabKid: eak.ID, + EabHmacKey: eak.KeyBytes, ProvisionerName: eak.ProvisionerName, - KeyID: eak.ID, Name: eak.Name, - Key: eak.KeyBytes, } - api.JSONStatus(w, eakResponse, http.StatusCreated) // TODO: rewrite into protobuf json (likely) + api.ProtoJSONStatus(w, response, http.StatusCreated) } // GetExternalAccountKeys returns a segment of ACME EAB Keys. diff --git a/ca/adminClient.go b/ca/adminClient.go index 7f24efa9..06462051 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -12,7 +12,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" adminAPI "github.com/smallstep/certificates/authority/admin/api" "github.com/smallstep/certificates/authority/provisioner" @@ -600,7 +599,7 @@ retry: } // CreateExternalAccountKey performs the POST /admin/acme/eab request to the CA. -func (c *AdminClient) CreateExternalAccountKey(eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*adminAPI.CreateExternalAccountKeyResponse, error) { +func (c *AdminClient) CreateExternalAccountKey(eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*linkedca.EABKey, error) { var retried bool body, err := json.Marshal(eakRequest) if err != nil { @@ -628,18 +627,18 @@ retry: } return nil, readAdminError(resp.Body) } - var eakResp = new(adminAPI.CreateExternalAccountKeyResponse) - if err := api.ReadJSON(resp.Body, &eakResp); err != nil { + var eabKey = new(linkedca.EABKey) + if err := readProtoJSON(resp.Body, eabKey); err != nil { return nil, errors.Wrapf(err, "error reading %s", u) } - return eakResp, nil + return eabKey, nil } // GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeys(opts ...AdminOption) ([]*adminAPI.CreateExternalAccountKeyResponse, error) { +func (c *AdminClient) GetExternalAccountKeys(opts ...AdminOption) ([]*linkedca.EABKey, error) { var ( cursor = "" - eaks = []*adminAPI.CreateExternalAccountKeyResponse{} + eaks = []*linkedca.EABKey{} ) for { resp, err := c.GetExternalAccountKeysPaginate(WithAdminCursor(cursor), WithAdminLimit(100)) From 9d09f5e5759fa629cca5e9bd936b35bd64686290 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 27 Aug 2021 14:10:00 +0200 Subject: [PATCH 14/47] Add support for deleting ACME EAB keys --- acme/db.go | 13 +++++++++++++ acme/db/nosql/account.go | 8 ++++++++ authority/admin/api/acme.go | 13 +++++++++++++ authority/admin/api/handler.go | 1 + ca/adminClient.go | 28 ++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+) diff --git a/acme/db.go b/acme/db.go index 46e3fc97..5c490410 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,6 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + DeleteExternalAccountKey(ctx context.Context, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error CreateNonce(ctx context.Context) (Nonce, error) @@ -53,6 +54,7 @@ type MockDB struct { MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + MockDeleteExternalAccountKey func(ctx context.Context, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockCreateNonce func(ctx context.Context) (Nonce, error) @@ -138,6 +140,17 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName stri return m.MockRet1.(*ExternalAccountKey), m.MockError } +// DeleteExternalAccountKey mock +func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, keyID string) error { + if m.MockDeleteExternalAccountKey != nil { + return m.MockDeleteExternalAccountKey(ctx, keyID) + } else if m.MockError != nil { + return m.MockError + } + return m.MockError +} + +// UpdateExternalAccountKey mock func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error { if m.MockUpdateExternalAccountKey != nil { return m.MockUpdateExternalAccountKey(ctx, provisionerName, eak) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index dc909dde..a0a43ffd 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -221,6 +221,14 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, }, nil } +func (db *DB) DeleteExternalAccountKey(ctx context.Context, keyID string) error { + err := db.db.Del(externalAccountKeyTable, []byte(keyID)) + if err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID: %s", keyID) + } + return nil +} + func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { old, err := db.getDBExternalAccountKey(ctx, eak.ID) if err != nil { diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 7772e435..c96b54b4 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -3,6 +3,7 @@ package api import ( "net/http" + "github.com/go-chi/chi" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "go.step.sm/linkedca" @@ -60,6 +61,18 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques api.ProtoJSONStatus(w, response, http.StatusCreated) } +// DeleteExternalAccountKey deletes an ACME External Account Key. +func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + + if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), id); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", id)) + return + } + + api.JSON(w, &DeleteResponse{Status: "ok"}) +} + // GetExternalAccountKeys returns a segment of ACME EAB Keys. func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { // cursor, limit, err := api.ParseCursor(r) diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 0e49dfd9..4dd21796 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -46,4 +46,5 @@ func (h *Handler) Route(r api.Router) { // ACME External Account Binding Keys r.MethodFunc("GET", "/acme/eab", authnz(h.GetExternalAccountKeys)) r.MethodFunc("POST", "/acme/eab", authnz(h.CreateExternalAccountKey)) + r.MethodFunc("DELETE", "/acme/eab/{id}", authnz(h.DeleteExternalAccountKey)) } diff --git a/ca/adminClient.go b/ca/adminClient.go index 06462051..c52fc38e 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -634,6 +634,34 @@ retry: return eabKey, nil } +// RemoveExternalAccountKey performs the DELETE /admin/acme/eab/{key_id} request to the CA. +func (c *AdminClient) RemoveExternalAccountKey(keyID string) error { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", keyID)}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return errors.Wrapf(err, "error generating admin token") + } + req, err := http.NewRequest("DELETE", u.String(), nil) + if err != nil { + return errors.Wrapf(err, "create DELETE %s request failed", u) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return errors.Wrapf(err, "client DELETE %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readAdminError(resp.Body) + } + return nil +} + // GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. func (c *AdminClient) GetExternalAccountKeys(opts ...AdminOption) ([]*linkedca.EABKey, error) { var ( From a1afbce50c182857d1ca2db0b4c00778bc3d6a46 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 27 Aug 2021 14:47:10 +0200 Subject: [PATCH 15/47] Check EAB key exists before deleting it --- acme/db/nosql/account.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index a0a43ffd..864ab119 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -222,9 +222,13 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, } func (db *DB) DeleteExternalAccountKey(ctx context.Context, keyID string) error { - err := db.db.Del(externalAccountKeyTable, []byte(keyID)) + _, err := db.db.Get(externalAccountKeyTable, []byte(keyID)) if err != nil { - return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID: %s", keyID) + return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) + } + err = db.db.Del(externalAccountKeyTable, []byte(keyID)) + if err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) } return nil } From f11c0cdc0c6f515dc35ba5f60baa47faae04c8b7 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 27 Aug 2021 16:58:04 +0200 Subject: [PATCH 16/47] Add endpoint for listing ACME EAB keys --- acme/db.go | 12 +++++++++++ acme/db/nosql/account.go | 27 +++++++++++++++++++++++ authority/admin/api/acme.go | 39 +++++++++++++++++++++++++--------- authority/admin/api/handler.go | 2 +- ca/adminClient.go | 19 ++++++++--------- 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/acme/db.go b/acme/db.go index 5c490410..90eb85aa 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,6 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) DeleteExternalAccountKey(ctx context.Context, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error @@ -54,6 +55,7 @@ type MockDB struct { MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) MockDeleteExternalAccountKey func(ctx context.Context, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error @@ -140,6 +142,16 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName stri return m.MockRet1.(*ExternalAccountKey), m.MockError } +// GetExternalAccountKeys mock +func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) { + if m.MockGetExternalAccountKeys != nil { + return m.MockGetExternalAccountKeys(ctx, provisionerName) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.([]*ExternalAccountKey), m.MockError +} + // DeleteExternalAccountKey mock func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, keyID string) error { if m.MockDeleteExternalAccountKey != nil { diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 864ab119..8647c5e3 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -233,6 +233,33 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, keyID string) error return nil } +// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { + entries, err := db.db.List(externalAccountKeyTable) + if err != nil { + return nil, err + } + + keys := make([]*acme.ExternalAccountKey, len(entries)) + for i, entry := range entries { + dbeak := new(dbExternalAccountKey) + if err = json.Unmarshal(entry.Value, dbeak); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", string(entry.Key)) + } + keys[i] = &acme.ExternalAccountKey{ + ID: dbeak.ID, + KeyBytes: dbeak.KeyBytes, + ProvisionerName: dbeak.ProvisionerName, + Name: dbeak.Name, + AccountID: dbeak.AccountID, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + } + } + + return keys, nil +} + func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { old, err := db.getDBExternalAccountKey(ctx, eak.ID) if err != nil { diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index c96b54b4..e8b26174 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -7,6 +7,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/timestamppb" ) // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests @@ -35,7 +36,7 @@ type GetExternalAccountKeysResponse struct { // CreateExternalAccountKey creates a new External Account Binding key func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { var body CreateExternalAccountKeyRequest - if err := api.ReadJSON(r.Body, &body); err != nil { // TODO: rewrite into protobuf json (likely) + if err := api.ReadJSON(r.Body, &body); err != nil { api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body")) return } @@ -75,6 +76,9 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques // GetExternalAccountKeys returns a segment of ACME EAB Keys. func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { + prov := chi.URLParam(r, "prov") + + // TODO: support paging properly? It'll probably leak to the DB layer, as we have to loop through all keys // cursor, limit, err := api.ParseCursor(r) // if err != nil { // api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, @@ -82,13 +86,28 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) // return // } - // eaks, nextCursor, err := h.acmeDB.GetExternalAccountKeys(cursor, limit) - // if err != nil { - // api.WriteError(w, admin.WrapErrorISE(err, "error retrieving paginated admins")) - // return - // } - // api.JSON(w, &GetExternalAccountKeysResponse{ - // EAKs: eaks, - // NextCursor: nextCursor, - // }) + keys, err := h.acmeDB.GetExternalAccountKeys(r.Context(), prov) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) + return + } + + eaks := make([]*linkedca.EABKey, len(keys)) + for i, k := range keys { + eaks[i] = &linkedca.EABKey{ + EabKid: k.ID, + EabHmacKey: []byte{}, + ProvisionerName: k.ProvisionerName, + Name: k.Name, + Account: k.AccountID, + CreatedAt: timestamppb.New(k.CreatedAt), + BoundAt: timestamppb.New(k.BoundAt), + } + } + + nextCursor := "" + api.JSON(w, &GetExternalAccountKeysResponse{ + EAKs: eaks, + NextCursor: nextCursor, + }) } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 4dd21796..694d3595 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -44,7 +44,7 @@ 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", authnz(h.GetExternalAccountKeys)) + r.MethodFunc("GET", "/acme/eab/{prov}", authnz(h.GetExternalAccountKeys)) r.MethodFunc("POST", "/acme/eab", authnz(h.CreateExternalAccountKey)) r.MethodFunc("DELETE", "/acme/eab/{id}", authnz(h.DeleteExternalAccountKey)) } diff --git a/ca/adminClient.go b/ca/adminClient.go index c52fc38e..a9865b1b 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -559,14 +559,14 @@ retry: } // GetExternalAccountKeysPaginate returns a page from the the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeysPaginate(opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { +func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { var retried bool o := new(adminOptions) if err := o.apply(opts); err != nil { return nil, err } u := c.endpoint.ResolveReference(&url.URL{ - Path: "/admin/acme/eab", + Path: path.Join(adminURLPrefix, "acme/eab", provisionerName), RawQuery: o.rawQuery(), }) tok, err := c.generateAdminToken(u.Path) @@ -590,12 +590,11 @@ retry: } return nil, readAdminError(resp.Body) } - // var body = new(GetExternalAccountKeysResponse) - // if err := readJSON(resp.Body, body); err != nil { - // return nil, errors.Wrapf(err, "error reading %s", u) - // } - // return body, nil - return nil, nil // TODO: fix correctly + var body = new(adminAPI.GetExternalAccountKeysResponse) + if err := readJSON(resp.Body, body); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return body, nil } // CreateExternalAccountKey performs the POST /admin/acme/eab request to the CA. @@ -663,13 +662,13 @@ retry: } // GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeys(opts ...AdminOption) ([]*linkedca.EABKey, error) { +func (c *AdminClient) GetExternalAccountKeys(provisionerName string, opts ...AdminOption) ([]*linkedca.EABKey, error) { var ( cursor = "" eaks = []*linkedca.EABKey{} ) for { - resp, err := c.GetExternalAccountKeysPaginate(WithAdminCursor(cursor), WithAdminLimit(100)) + resp, err := c.GetExternalAccountKeysPaginate(provisionerName, WithAdminCursor(cursor), WithAdminLimit(100)) if err != nil { return nil, err } From 02cd3b6b3bf763662b24bef35279b07dd000b107 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 16 Sep 2021 23:09:24 +0200 Subject: [PATCH 17/47] Fix PR comments --- acme/account.go | 14 +++--- acme/api/account.go | 2 +- acme/api/account_test.go | 52 ++++++++++---------- acme/db/nosql/account.go | 95 ++++++++++++++++++------------------ authority/admin/api/acme.go | 96 +++++++++++++++++++++++++++++-------- 5 files changed, 160 insertions(+), 99 deletions(-) diff --git a/acme/account.go b/acme/account.go index e9d3ac7f..deaf57c8 100644 --- a/acme/account.go +++ b/acme/account.go @@ -44,13 +44,13 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { } type ExternalAccountKey struct { - ID string `json:"id"` - ProvisionerName string `json:"provisionerName"` - Name string `json:"name"` - AccountID string `json:"-"` - KeyBytes []byte `json:"-"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt,omitempty"` + ID string `json:"id"` + Provisioner string `json:"provisioner"` + Reference string `json:"reference"` + AccountID string `json:"-"` + KeyBytes []byte `json:"-"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt,omitempty"` } func (eak *ExternalAccountKey) AlreadyBound() bool { diff --git a/acme/api/account.go b/acme/api/account.go index fa5597e0..8d814d1c 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -257,7 +257,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc // about the handler and thus about its dependencies. eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) if err != nil { - return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into JSON") + return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes") } eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index f6a368d8..bced48b2 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -686,11 +686,11 @@ func TestHandler_NewAccount(t *testing.T) { }, MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerName: escProvName, - Name: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), }, nil }, MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { @@ -1059,11 +1059,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerName: escProvName, - Name: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), }, nil }, }, @@ -1200,12 +1200,12 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerName: escProvName, - Name: "testeak", - CreatedAt: createdAt, - AccountID: "some-account-id", - BoundAt: boundAt, + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + CreatedAt: createdAt, + AccountID: "some-account-id", + BoundAt: boundAt, }, nil }, }, @@ -1235,11 +1235,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerName: escProvName, - Name: "testeak", - KeyBytes: []byte{1, 2, 3, 4}, - CreatedAt: time.Now(), + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 2, 3, 4}, + CreatedAt: time.Now(), }, nil }, }, @@ -1271,11 +1271,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - ProvisionerName: escProvName, - Name: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), }, nil }, }, diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 8647c5e3..d2572fbf 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -28,13 +28,13 @@ func (dba *dbAccount) clone() *dbAccount { } type dbExternalAccountKey struct { - ID string `json:"id"` - ProvisionerName string `json:"provisioner_name"` - Name string `json:"name"` - AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt"` + ID string `json:"id"` + Provisioner string `json:"provisioner"` + Reference string `json:"reference"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` } func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) { @@ -165,7 +165,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { } // CreateExternalAccountKey creates a new External Account Binding key with a name -func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*acme.ExternalAccountKey, error) { +func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName string, reference string) (*acme.ExternalAccountKey, error) { keyID, err := randID() if err != nil { return nil, err @@ -178,24 +178,24 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName stri } dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerName: provisionerName, - Name: name, - KeyBytes: random, - CreatedAt: clock.Now(), + ID: keyID, + Provisioner: provisionerName, + Reference: reference, + KeyBytes: random, + CreatedAt: clock.Now(), } if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { return nil, err } return &acme.ExternalAccountKey{ - ID: dbeak.ID, - ProvisionerName: dbeak.ProvisionerName, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + Provisioner: dbeak.Provisioner, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, nil } @@ -206,18 +206,18 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, return nil, err } - if dbeak.ProvisionerName != provisionerName { + if dbeak.Provisioner != provisionerName { return nil, acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") } return &acme.ExternalAccountKey{ - ID: dbeak.ID, - ProvisionerName: dbeak.ProvisionerName, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + Provisioner: dbeak.Provisioner, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, nil } @@ -240,21 +240,24 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string return nil, err } - keys := make([]*acme.ExternalAccountKey, len(entries)) - for i, entry := range entries { + keys := []*acme.ExternalAccountKey{} + for _, entry := range entries { dbeak := new(dbExternalAccountKey) if err = json.Unmarshal(entry.Value, dbeak); err != nil { return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", string(entry.Key)) } - keys[i] = &acme.ExternalAccountKey{ - ID: dbeak.ID, - KeyBytes: dbeak.KeyBytes, - ProvisionerName: dbeak.ProvisionerName, - Name: dbeak.Name, - AccountID: dbeak.AccountID, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + if dbeak.Provisioner != provisionerName { + continue } + keys = append(keys, &acme.ExternalAccountKey{ + ID: dbeak.ID, + KeyBytes: dbeak.KeyBytes, + Provisioner: dbeak.Provisioner, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }) } return keys, nil @@ -266,18 +269,18 @@ func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName stri return err } - if old.ProvisionerName != provisionerName { + if old.Provisioner != provisionerName { return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") } nu := dbExternalAccountKey{ - ID: eak.ID, - ProvisionerName: eak.ProvisionerName, - Name: eak.Name, - AccountID: eak.AccountID, - KeyBytes: eak.KeyBytes, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, + ID: eak.ID, + Provisioner: eak.Provisioner, + Reference: eak.Reference, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, } return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index e8b26174..6889764b 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -1,28 +1,30 @@ package api import ( + "context" "net/http" "github.com/go-chi/chi" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" "google.golang.org/protobuf/types/known/timestamppb" ) // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests type CreateExternalAccountKeyRequest struct { - ProvisionerName string `json:"provisioner"` - Name string `json:"name"` + Provisioner string `json:"provisioner"` + Reference string `json:"reference"` } -// Validate validates a new-admin request body. +// Validate validates a new ACME EAB Key request body. func (r *CreateExternalAccountKeyRequest) Validate() error { - if r.ProvisionerName == "" { + if r.Provisioner == "" { return admin.NewError(admin.ErrorBadRequestType, "provisioner name cannot be empty") } - if r.Name == "" { - return admin.NewError(admin.ErrorBadRequestType, "name / reference cannot be empty") + if r.Reference == "" { + return admin.NewError(admin.ErrorBadRequestType, "reference cannot be empty") } return nil } @@ -33,6 +35,38 @@ type GetExternalAccountKeysResponse struct { NextCursor string `json:"nextCursor"` } +// provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME +// provisioner is set to true and thus has EAB enabled. +// TODO: rewrite this into a middleware for the ACME handlers? This probably requires +// ensuring that all the ACME EAB APIs that need the middleware work the same in terms +// of specifying the provisioner; probably a bit of refactoring required. +func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, error) { + var ( + p provisioner.Interface + err error + ) + if p, err = h.auth.LoadProvisionerByName(provisionerName); err != nil { + return false, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName) + } + + prov, err := h.db.GetProvisioner(ctx, p.GetID()) + if err != nil { + return false, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID()) + } + + details := prov.GetDetails() + if details == nil { + return false, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID()) + } + + acme := details.GetACME() + if acme == nil { + return false, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) + } + + return acme.GetRequireEab(), nil +} + // CreateExternalAccountKey creates a new External Account Binding key func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { var body CreateExternalAccountKeyRequest @@ -46,17 +80,28 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques return } - eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.ProvisionerName, body.Name) + eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), body.Provisioner) + if err != nil { + api.WriteError(w, err) + return + } + + if !eabEnabled { + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", body.Provisioner)) + return + } + + eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Provisioner, body.Reference) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) + api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s for provisioner %s", body.Reference, body.Provisioner)) return } response := &linkedca.EABKey{ - EabKid: eak.ID, - EabHmacKey: eak.KeyBytes, - ProvisionerName: eak.ProvisionerName, - Name: eak.Name, + Id: eak.ID, + HmacKey: eak.KeyBytes, + Provisioner: eak.Provisioner, + Reference: eak.Reference, } api.ProtoJSONStatus(w, response, http.StatusCreated) @@ -66,6 +111,8 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") + // TODO: add provisioner as parameter, so that check can be performed if EAB is enabled or not + if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), id); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", id)) return @@ -78,6 +125,17 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { prov := chi.URLParam(r, "prov") + eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), prov) + if err != nil { + api.WriteError(w, err) + return + } + + if !eabEnabled { + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov)) + return + } + // TODO: support paging properly? It'll probably leak to the DB layer, as we have to loop through all keys // cursor, limit, err := api.ParseCursor(r) // if err != nil { @@ -95,13 +153,13 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) eaks := make([]*linkedca.EABKey, len(keys)) for i, k := range keys { eaks[i] = &linkedca.EABKey{ - EabKid: k.ID, - EabHmacKey: []byte{}, - ProvisionerName: k.ProvisionerName, - Name: k.Name, - Account: k.AccountID, - CreatedAt: timestamppb.New(k.CreatedAt), - BoundAt: timestamppb.New(k.BoundAt), + Id: k.ID, + HmacKey: []byte{}, + Provisioner: k.Provisioner, + Reference: k.Reference, + Account: k.AccountID, + CreatedAt: timestamppb.New(k.CreatedAt), + BoundAt: timestamppb.New(k.BoundAt), } } From 9c0020352bc8b05158be398efbd648d2b5c78b4b Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 17 Sep 2021 17:08:02 +0200 Subject: [PATCH 18/47] Add lookup by reference and make reference optional --- acme/db.go | 22 +++++++++++++++++----- acme/db/nosql/account.go | 32 ++++++++++++++++++++++++++++++++ acme/db/nosql/nosql.go | 21 +++++++++++---------- authority/admin/api/acme.go | 26 +++++++++++++++++++------- authority/admin/api/handler.go | 1 + ca/adminClient.go | 12 ++++++++---- 6 files changed, 88 insertions(+), 26 deletions(-) diff --git a/acme/db.go b/acme/db.go index 90eb85aa..59a7beb0 100644 --- a/acme/db.go +++ b/acme/db.go @@ -22,6 +22,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) + GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) DeleteExternalAccountKey(ctx context.Context, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error @@ -53,11 +54,12 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error - MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) - MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) - MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) - MockDeleteExternalAccountKey func(ctx context.Context, keyID string) error - MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error + MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) + MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) + MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) + MockDeleteExternalAccountKey func(ctx context.Context, keyID string) error + MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockCreateNonce func(ctx context.Context) (Nonce, error) MockDeleteNonce func(ctx context.Context, nonce Nonce) error @@ -152,6 +154,16 @@ func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName str return m.MockRet1.([]*ExternalAccountKey), m.MockError } +// GetExtrnalAccountKeyByReference mock +func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) { + if m.MockGetExternalAccountKeys != nil { + return m.GetExternalAccountKeyByReference(ctx, provisionerName, reference) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(*ExternalAccountKey), m.MockError +} + // DeleteExternalAccountKey mock func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, keyID string) error { if m.MockDeleteExternalAccountKey != nil { diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index d2572fbf..611d686b 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/acme" + "github.com/smallstep/nosql" nosqlDB "github.com/smallstep/nosql" "go.step.sm/crypto/jose" ) @@ -37,6 +38,11 @@ type dbExternalAccountKey struct { BoundAt time.Time `json:"boundAt"` } +type dbExternalAccountKeyReference struct { + Reference string `json:"reference"` + ExternalAccountKeyID string `json:"externalAccountKeyID"` +} + func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) { id, err := db.db.Get(accountByKeyIDTable, []byte(kid)) if err != nil { @@ -188,6 +194,17 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName stri if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { return nil, err } + + if dbeak.Reference != "" { + dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ + Reference: dbeak.Reference, + ExternalAccountKeyID: dbeak.ID, + } + if err = db.save(ctx, dbeak.Reference, dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { + return nil, err + } + } + return &acme.ExternalAccountKey{ ID: dbeak.ID, Provisioner: dbeak.Provisioner, @@ -263,6 +280,21 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string return keys, nil } +// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference +func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*acme.ExternalAccountKey, error) { + k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(reference)) + if nosql.IsErrNotFound(err) { + return nil, errors.Errorf("ACME EAB key for reference %s not found", reference) + } else if err != nil { + return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) + } + dbExternalAccountKeyReference := new(dbExternalAccountKeyReference) + if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) + } + return db.GetExternalAccountKey(ctx, provisionerName, dbExternalAccountKeyReference.ExternalAccountKeyID) +} + func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { old, err := db.getDBExternalAccountKey(ctx, eak.ID) if err != nil { diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 320e7d58..1e02f48e 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -11,15 +11,16 @@ import ( ) var ( - accountTable = []byte("acme_accounts") - accountByKeyIDTable = []byte("acme_keyID_accountID_index") - authzTable = []byte("acme_authzs") - challengeTable = []byte("acme_challenges") - nonceTable = []byte("nonces") - orderTable = []byte("acme_orders") - ordersByAccountIDTable = []byte("acme_account_orders_index") - certTable = []byte("acme_certs") - externalAccountKeyTable = []byte("acme_external_account_keys") + accountTable = []byte("acme_accounts") + accountByKeyIDTable = []byte("acme_keyID_accountID_index") + authzTable = []byte("acme_authzs") + challengeTable = []byte("acme_challenges") + nonceTable = []byte("nonces") + orderTable = []byte("acme_orders") + ordersByAccountIDTable = []byte("acme_account_orders_index") + certTable = []byte("acme_certs") + externalAccountKeyTable = []byte("acme_external_account_keys") + externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") ) // DB is a struct that implements the AcmeDB interface. @@ -30,7 +31,7 @@ type DB struct { // New configures and returns a new ACME DB backend implemented using a nosql DB. func New(db nosqlDB.DB) (*DB, error) { tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, - challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, externalAccountKeyTable} + challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, externalAccountKeyTable, externalAccountKeysByReferenceTable} for _, b := range tables { if err := db.CreateTable(b); err != nil { return nil, errors.Wrapf(err, "error creating table %s", diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 6889764b..3aa4ed31 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/go-chi/chi" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" @@ -23,9 +24,6 @@ func (r *CreateExternalAccountKeyRequest) Validate() error { if r.Provisioner == "" { return admin.NewError(admin.ErrorBadRequestType, "provisioner name cannot be empty") } - if r.Reference == "" { - return admin.NewError(admin.ErrorBadRequestType, "reference cannot be empty") - } return nil } @@ -124,6 +122,7 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques // GetExternalAccountKeys returns a segment of ACME EAB Keys. func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { prov := chi.URLParam(r, "prov") + reference := chi.URLParam(r, "ref") eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), prov) if err != nil { @@ -144,10 +143,23 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) // return // } - keys, err := h.acmeDB.GetExternalAccountKeys(r.Context(), prov) - if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) - return + var ( + key *acme.ExternalAccountKey + keys []*acme.ExternalAccountKey + ) + if reference != "" { + key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error getting external account key with reference %s", reference)) + return + } + keys = []*acme.ExternalAccountKey{key} + } else { + keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) + return + } } eaks := make([]*linkedca.EABKey, len(keys)) diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 694d3595..2746a3df 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -44,6 +44,7 @@ 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(h.GetExternalAccountKeys)) r.MethodFunc("GET", "/acme/eab/{prov}", authnz(h.GetExternalAccountKeys)) r.MethodFunc("POST", "/acme/eab", authnz(h.CreateExternalAccountKey)) r.MethodFunc("DELETE", "/acme/eab/{id}", authnz(h.DeleteExternalAccountKey)) diff --git a/ca/adminClient.go b/ca/adminClient.go index a9865b1b..8e1202d4 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -559,14 +559,18 @@ retry: } // GetExternalAccountKeysPaginate returns a page from the the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { +func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName string, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, 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: path.Join(adminURLPrefix, "acme/eab", provisionerName), + Path: p, RawQuery: o.rawQuery(), }) tok, err := c.generateAdminToken(u.Path) @@ -662,13 +666,13 @@ retry: } // GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeys(provisionerName string, opts ...AdminOption) ([]*linkedca.EABKey, error) { +func (c *AdminClient) GetExternalAccountKeys(provisionerName string, reference string, opts ...AdminOption) ([]*linkedca.EABKey, error) { var ( cursor = "" eaks = []*linkedca.EABKey{} ) for { - resp, err := c.GetExternalAccountKeysPaginate(provisionerName, WithAdminCursor(cursor), WithAdminLimit(100)) + resp, err := c.GetExternalAccountKeysPaginate(provisionerName, reference, WithAdminCursor(cursor), WithAdminLimit(100)) if err != nil { return nil, err } From 746c5c9fd947aab74cb7376d5b32e2056770a924 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 17 Sep 2021 17:25:19 +0200 Subject: [PATCH 19/47] Disallow creation of EAB keys with non-unique references --- acme/db/nosql/account.go | 3 +++ authority/admin/api/acme.go | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 611d686b..b52489bd 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -282,6 +282,9 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*acme.ExternalAccountKey, error) { + if reference == "" { + return nil, nil + } k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(reference)) if nosql.IsErrNotFound(err) { return nil, errors.Errorf("ACME EAB key for reference %s not found", reference) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 3aa4ed31..de541a23 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -78,20 +78,33 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques return } - eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), body.Provisioner) + provisioner := body.Provisioner + reference := body.Reference + + eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), provisioner) if err != nil { api.WriteError(w, err) return } if !eabEnabled { - api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", body.Provisioner)) + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", provisioner)) return } - eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Provisioner, body.Reference) + if reference != "" { + k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), provisioner, reference) + if err == nil || k != nil { + err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner %s with reference %s already exists", provisioner, reference) + err.Status = 409 + api.WriteError(w, err) + return + } + } + + eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), provisioner, reference) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s for provisioner %s", body.Reference, body.Provisioner)) + api.WriteError(w, admin.WrapErrorISE(err, "error creating ACME EAB key for provisioner %s and reference %s", provisioner, reference)) return } From c2bc1351c6bedd99efad9757480de4e0db0acf6c Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 17 Sep 2021 17:48:09 +0200 Subject: [PATCH 20/47] Add provisioner to remove endpoint and clear reference index on delete --- acme/db.go | 8 ++++---- acme/db/nosql/account.go | 17 +++++++++++++---- authority/admin/api/acme.go | 18 ++++++++++++++---- authority/admin/api/handler.go | 2 +- ca/adminClient.go | 6 +++--- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/acme/db.go b/acme/db.go index 59a7beb0..aa81f7d3 100644 --- a/acme/db.go +++ b/acme/db.go @@ -23,7 +23,7 @@ type DB interface { GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) - DeleteExternalAccountKey(ctx context.Context, keyID string) error + DeleteExternalAccountKey(ctx context.Context, provisionerName string, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error CreateNonce(ctx context.Context) (Nonce, error) @@ -58,7 +58,7 @@ type MockDB struct { MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) - MockDeleteExternalAccountKey func(ctx context.Context, keyID string) error + MockDeleteExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockCreateNonce func(ctx context.Context) (Nonce, error) @@ -165,9 +165,9 @@ func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provision } // DeleteExternalAccountKey mock -func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, keyID string) error { +func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerName string, keyID string) error { if m.MockDeleteExternalAccountKey != nil { - return m.MockDeleteExternalAccountKey(ctx, keyID) + return m.MockDeleteExternalAccountKey(ctx, provisionerName, keyID) } else if m.MockError != nil { return m.MockError } diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index b52489bd..1cf94b37 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -8,7 +8,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/acme" - "github.com/smallstep/nosql" nosqlDB "github.com/smallstep/nosql" "go.step.sm/crypto/jose" ) @@ -238,11 +237,21 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, }, nil } -func (db *DB) DeleteExternalAccountKey(ctx context.Context, keyID string) error { - _, err := db.db.Get(externalAccountKeyTable, []byte(keyID)) +func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName string, keyID string) error { + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) } + if dbeak.Provisioner != provisionerName { + // TODO: change these ACME error types; they don't make a lot of sense if used in the Admin APIs + return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + } + if dbeak.Reference != "" { + err = db.db.Del(externalAccountKeysByReferenceTable, []byte(dbeak.Reference)) + if err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key Reference with Key ID %s and reference %s", keyID, dbeak.Reference) + } + } err = db.db.Del(externalAccountKeyTable, []byte(keyID)) if err != nil { return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) @@ -286,7 +295,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerN return nil, nil } k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(reference)) - if nosql.IsErrNotFound(err) { + if nosqlDB.IsErrNotFound(err) { return nil, errors.Errorf("ACME EAB key for reference %s not found", reference) } else if err != nil { return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index de541a23..ceac9320 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -120,12 +120,22 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques // DeleteExternalAccountKey deletes an ACME External Account Key. func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") + provisioner := chi.URLParam(r, "prov") + keyID := chi.URLParam(r, "id") - // TODO: add provisioner as parameter, so that check can be performed if EAB is enabled or not + eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), provisioner) + if err != nil { + api.WriteError(w, err) + return + } + + if !eabEnabled { + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", provisioner)) + return + } - if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), id); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", id)) + if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), provisioner, keyID); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", keyID)) return } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 2746a3df..95e12d07 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -47,5 +47,5 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("GET", "/acme/eab/{prov}/{ref}", authnz(h.GetExternalAccountKeys)) r.MethodFunc("GET", "/acme/eab/{prov}", authnz(h.GetExternalAccountKeys)) r.MethodFunc("POST", "/acme/eab", authnz(h.CreateExternalAccountKey)) - r.MethodFunc("DELETE", "/acme/eab/{id}", authnz(h.DeleteExternalAccountKey)) + r.MethodFunc("DELETE", "/acme/eab/{prov}/{id}", authnz(h.DeleteExternalAccountKey)) } diff --git a/ca/adminClient.go b/ca/adminClient.go index 8e1202d4..582c99b9 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -637,10 +637,10 @@ retry: return eabKey, nil } -// RemoveExternalAccountKey performs the DELETE /admin/acme/eab/{key_id} request to the CA. -func (c *AdminClient) RemoveExternalAccountKey(keyID string) error { +// RemoveExternalAccountKey performs the DELETE /admin/acme/eab/{prov}/{key_id} request to the CA. +func (c *AdminClient) RemoveExternalAccountKey(provisionerName string, keyID string) error { var retried bool - u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", keyID)}) + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", provisionerName, "/", keyID)}) tok, err := c.generateAdminToken(u.Path) if err != nil { return errors.Wrapf(err, "error generating admin token") From 0afea2e95771e2f641d2e5b54ac480c18180d0fd Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 8 Oct 2021 13:18:23 +0200 Subject: [PATCH 21/47] Improve tests for already bound EAB keys --- acme/account.go | 11 ++++++- acme/account_test.go | 65 ++++++++++++++++++++++++++++++++++++++++ acme/api/account.go | 6 +++- acme/api/account_test.go | 28 +++++++++-------- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/acme/account.go b/acme/account.go index deaf57c8..14a707e9 100644 --- a/acme/account.go +++ b/acme/account.go @@ -43,6 +43,7 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { return base64.RawURLEncoding.EncodeToString(kid), nil } +// ExternalAccountKey is an ACME External Account Binding key. type ExternalAccountKey struct { ID string `json:"id"` Provisioner string `json:"provisioner"` @@ -53,12 +54,20 @@ type ExternalAccountKey struct { BoundAt time.Time `json:"boundAt,omitempty"` } +// AlreadyBound returns whether this EAK is already bound to +// an ACME Account or not. func (eak *ExternalAccountKey) AlreadyBound() bool { return !eak.BoundAt.IsZero() } -func (eak *ExternalAccountKey) BindTo(account *Account) { +// BindTo binds the EAK to an Account. +// It returns an error if it's already bound. +func (eak *ExternalAccountKey) BindTo(account *Account) error { + if eak.AlreadyBound() { + return NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", eak.ID, eak.AccountID, eak.BoundAt) + } eak.AccountID = account.ID eak.BoundAt = time.Now() eak.KeyBytes = []byte{} // clearing the key bytes; can only be used once + return nil } diff --git a/acme/account_test.go b/acme/account_test.go index 5625c3dc..44b815b9 100644 --- a/acme/account_test.go +++ b/acme/account_test.go @@ -4,6 +4,7 @@ import ( "crypto" "encoding/base64" "testing" + "time" "github.com/pkg/errors" "github.com/smallstep/assert" @@ -79,3 +80,67 @@ func TestAccount_IsValid(t *testing.T) { }) } } + +func TestExternalAccountKey_BindTo(t *testing.T) { + boundAt := time.Now() + tests := []struct { + name string + eak *ExternalAccountKey + acct *Account + err *Error + }{ + { + name: "ok", + eak: &ExternalAccountKey{ + ID: "eakID", + Provisioner: "prov", + Reference: "ref", + KeyBytes: []byte{1, 3, 3, 7}, + }, + acct: &Account{ + ID: "accountID", + }, + err: nil, + }, + { + name: "fail/already-bound", + eak: &ExternalAccountKey{ + ID: "eakID", + Provisioner: "prov", + Reference: "ref", + KeyBytes: []byte{1, 3, 3, 7}, + AccountID: "someAccountID", + BoundAt: boundAt, + }, + acct: &Account{ + ID: "accountID", + }, + err: NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", "eakID", "someAccountID", boundAt), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eak := tt.eak + acct := tt.acct + err := eak.BindTo(acct) + wantErr := tt.err != nil + gotErr := err != nil + if wantErr != gotErr { + t.Errorf("ExternalAccountKey.BindTo() error = %v, wantErr %v", err, tt.err) + } + if wantErr { + assert.NotNil(t, err) + assert.Type(t, &Error{}, err) + ae, _ := err.(*Error) + assert.Equals(t, ae.Type, tt.err.Type) + assert.Equals(t, ae.Detail, tt.err.Detail) + assert.Equals(t, ae.Identifier, tt.err.Identifier) + assert.Equals(t, ae.Subproblems, tt.err.Subproblems) + } else { + assert.Equals(t, eak.AccountID, acct.ID) + assert.Equals(t, eak.KeyBytes, []byte{}) + assert.NotNil(t, eak.BoundAt) + } + }) + } +} diff --git a/acme/api/account.go b/acme/api/account.go index 8d814d1c..877f5773 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -138,7 +138,11 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response - eak.BindTo(acc) + err := eak.BindTo(acc) + if err != nil { + api.WriteError(w, err) + return + } if err := h.db.UpdateExternalAccountKey(ctx, prov.Name, eak); err != nil { api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key")) return diff --git a/acme/api/account_test.go b/acme/api/account_test.go index bced48b2..0bedc5d1 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -1055,6 +1055,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + createdAt := time.Now() return test{ db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { @@ -1063,7 +1064,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { Provisioner: escProvName, Reference: "testeak", KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + CreatedAt: createdAt, }, nil }, }, @@ -1072,7 +1073,13 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { Contact: []string{"foo", "bar"}, ExternalAccountBinding: eab, }, - eak: &acme.ExternalAccountKey{}, + eak: &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, err: nil, } }, @@ -1299,8 +1306,6 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { wantErr := tc.err != nil gotErr := err != nil if wantErr != gotErr { - // fmt.Println(got) - // fmt.Println(fmt.Sprintf("%#+v", got)) t.Errorf("Handler.validateExternalAccountBinding() error = %v, want %v", err, tc.err) } if wantErr { @@ -1311,20 +1316,19 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { assert.Equals(t, ae.Detail, tc.err.Detail) assert.Equals(t, ae.Identifier, tc.err.Identifier) assert.Equals(t, ae.Subproblems, tc.err.Subproblems) - - // fmt.Println(fmt.Sprintf("%#+v", ae)) - // fmt.Println(fmt.Sprintf("%#+v", tc.err)) - - //t.Fail() } else { if got == nil { assert.Nil(t, tc.eak) } else { - // TODO: equality check on certain fields? 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.Provisioner, tc.eak.Provisioner) + 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) } - //assert.Equals(t, tc.eak, got) - //assert.NotNil(t, got) } }) } From f34d68897a48ef91b1f430494b65111381728f56 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 8 Oct 2021 14:29:44 +0200 Subject: [PATCH 22/47] Refactor retrieval of provisioner into middleware --- authority/admin/api/acme.go | 64 ++++++++++++---------------------- authority/admin/api/handler.go | 12 ++++--- ca/adminClient.go | 4 +-- 3 files changed, 32 insertions(+), 48 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index ceac9320..700881dc 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -15,15 +15,11 @@ import ( // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests type CreateExternalAccountKeyRequest struct { - Provisioner string `json:"provisioner"` - Reference string `json:"reference"` + Reference string `json:"reference"` } // Validate validates a new ACME EAB Key request body. func (r *CreateExternalAccountKeyRequest) Validate() error { - if r.Provisioner == "" { - return admin.NewError(admin.ErrorBadRequestType, "provisioner name cannot be empty") - } return nil } @@ -33,11 +29,26 @@ type GetExternalAccountKeysResponse struct { NextCursor string `json:"nextCursor"` } +// requireEABEnabled is a middleware that ensures ACME EAB is enabled +// before serving requests that act on ACME EAB credentials. +func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + provisioner := chi.URLParam(r, "prov") + eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), provisioner) + if err != nil { + api.WriteError(w, err) + return + } + if !eabEnabled { + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", provisioner)) + return + } + next(w, r) + } +} + // provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME // provisioner is set to true and thus has EAB enabled. -// TODO: rewrite this into a middleware for the ACME handlers? This probably requires -// ensuring that all the ACME EAB APIs that need the middleware work the same in terms -// of specifying the provisioner; probably a bit of refactoring required. func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, error) { var ( p provisioner.Interface @@ -78,22 +89,12 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques return } - provisioner := body.Provisioner + provisioner := chi.URLParam(r, "prov") reference := body.Reference - eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), provisioner) - if err != nil { - api.WriteError(w, err) - return - } - - if !eabEnabled { - api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", provisioner)) - return - } - if reference != "" { k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), provisioner, reference) + // retrieving an EAB key from DB results in error if it doesn't exist, which is what we're looking for if err == nil || k != nil { err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner %s with reference %s already exists", provisioner, reference) err.Status = 409 @@ -123,17 +124,6 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques provisioner := chi.URLParam(r, "prov") keyID := chi.URLParam(r, "id") - eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), provisioner) - if err != nil { - api.WriteError(w, err) - return - } - - if !eabEnabled { - api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", provisioner)) - return - } - if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), provisioner, keyID); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", keyID)) return @@ -147,17 +137,6 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) prov := chi.URLParam(r, "prov") reference := chi.URLParam(r, "ref") - eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), prov) - if err != nil { - api.WriteError(w, err) - return - } - - if !eabEnabled { - api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov)) - return - } - // TODO: support paging properly? It'll probably leak to the DB layer, as we have to loop through all keys // cursor, limit, err := api.ParseCursor(r) // if err != nil { @@ -169,6 +148,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) var ( key *acme.ExternalAccountKey keys []*acme.ExternalAccountKey + err error ) if reference != "" { key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 95e12d07..ba13407d 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -29,6 +29,10 @@ func (h *Handler) Route(r api.Router) { return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) } + requireEABEnabled := func(next nextHTTP) nextHTTP { + return h.requireEABEnabled(next) + } + // Provisioners r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner)) r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners)) @@ -44,8 +48,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(h.GetExternalAccountKeys)) - r.MethodFunc("GET", "/acme/eab/{prov}", authnz(h.GetExternalAccountKeys)) - r.MethodFunc("POST", "/acme/eab", authnz(h.CreateExternalAccountKey)) - r.MethodFunc("DELETE", "/acme/eab/{prov}/{id}", authnz(h.DeleteExternalAccountKey)) + 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))) } diff --git a/ca/adminClient.go b/ca/adminClient.go index 582c99b9..0543ac94 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -602,13 +602,13 @@ retry: } // CreateExternalAccountKey performs the POST /admin/acme/eab request to the CA. -func (c *AdminClient) CreateExternalAccountKey(eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*linkedca.EABKey, error) { +func (c *AdminClient) CreateExternalAccountKey(provisionerName string, eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*linkedca.EABKey, error) { var retried bool body, err := json.Marshal(eakRequest) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } - u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab")}) + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab/", provisionerName)}) tok, err := c.generateAdminToken(u.Path) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") From c26041f835d421e63d96b0a96ae1c6684b8829a8 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 9 Oct 2021 01:02:00 +0200 Subject: [PATCH 23/47] Add ACME EAB nosql tests --- acme/db/nosql/account.go | 7 +- acme/db/nosql/account_test.go | 1017 +++++++++++++++++++++++++++++++++ authority/admin/api/acme.go | 8 +- 3 files changed, 1021 insertions(+), 11 deletions(-) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 1cf94b37..2f698d87 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -243,8 +243,7 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName stri return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) } if dbeak.Provisioner != provisionerName { - // TODO: change these ACME error types; they don't make a lot of sense if used in the Admin APIs - return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + return errors.New("name of provisioner does not match provisioner for which the EAB key was created") } if dbeak.Reference != "" { err = db.db.Del(externalAccountKeysByReferenceTable, []byte(dbeak.Reference)) @@ -270,7 +269,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string for _, entry := range entries { dbeak := new(dbExternalAccountKey) if err = json.Unmarshal(entry.Value, dbeak); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", string(entry.Key)) + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) } if dbeak.Provisioner != provisionerName { continue @@ -314,7 +313,7 @@ func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName stri } if old.Provisioner != provisionerName { - return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + return errors.New("name of provisioner does not match provisioner for which the EAB key was created") } nu := dbExternalAccountKey{ diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 5ba99a73..08b60f58 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -704,3 +704,1020 @@ func TestDB_UpdateAccount(t *testing.T) { }) } } + +func TestDB_getDBExternalAccountKey(t *testing.T) { + keyID := "keyID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + dbeak *dbExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "prov", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: nil, + dbeak: dbeak, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: acme.ErrNotFound, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return []byte("foo"), nil + }, + }, + err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if dbeak, err := db.getDBExternalAccountKey(context.Background(), keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.dbeak.ID) + assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) + assert.Equals(t, dbeak.Provisioner, tc.dbeak.Provisioner) + assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) + assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) + assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) + assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) + } + } + }) + } +} + +func TestDB_GetExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "aDifferentProv", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if eak, err := db.GetExternalAccountKey(context.Background(), prov, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.Reference, tc.eak.Reference) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + } + } + }) + } +} + +func TestDB_GetExternalAccountKeyByReference(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + ref string + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + err: nil, + } + }, + "ok/no-reference": func(t *testing.T) test { + return test{ + ref: "", + eak: nil, + err: nil, + } + }, + "fail/reference-not-found": func(t *testing.T) test { + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), ref) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("ACME EAB key for reference ref not found"), + } + }, + "fail/reference-load-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), ref) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB key for reference ref: force"), + } + }, + "fail/reference-unmarshal-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), ref) + return []byte{0}, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB key for reference ref"), + } + }, + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if eak, err := db.GetExternalAccountKeyByReference(context.Background(), prov, tc.ref); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) && tc.eak != nil { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.Reference, tc.eak.Reference) + } + } + }) + } +} + +func TestDB_GetExternalAccountKeys(t *testing.T) { + keyID1 := "keyID1" + keyID2 := "keyID2" + keyID3 := "keyID3" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak1 := &dbExternalAccountKey{ + ID: keyID1, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b1, err := json.Marshal(dbeak1) + assert.FatalError(t, err) + dbeak2 := &dbExternalAccountKey{ + ID: keyID2, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b2, err := json.Marshal(dbeak2) + assert.FatalError(t, err) + dbeak3 := &dbExternalAccountKey{ + ID: keyID3, + Provisioner: "differentProvisioner", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b3, err := json.Marshal(dbeak3) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: b1, + }, + { + Bucket: bucket, + Key: []byte(keyID2), + Value: b2, + }, + { + Bucket: bucket, + Key: []byte(keyID3), + Value: b3, + }, + }, nil + }, + }, + eaks: []*acme.ExternalAccountKey{ + { + ID: keyID1, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + { + ID: keyID2, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + }, + } + }, + "fail/db.List-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + return nil, errors.New("force") + }, + }, + err: errors.New("force"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: []byte("foo"), + }, + }, nil + }, + }, + eaks: []*acme.ExternalAccountKey{}, + err: errors.Errorf("error unmarshaling external account key %s into ExternalAccountKey", keyID1), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if eaks, err := db.GetExternalAccountKeys(context.Background(), prov); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, len(eaks), len(tc.eaks)) + for i, eak := range eaks { + assert.Equals(t, eak.ID, tc.eaks[i].ID) + assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eaks[i].Provisioner) + assert.Equals(t, eak.Reference, tc.eaks[i].Reference) + assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) + assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) + assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) + } + } + } + }) + } +} + +func TestDB_DeleteExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return errors.New("force") + } + }, + }, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("error loading ACME EAB Key with Key ID keyID"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "differentProvisioner", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/delete-reference": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return errors.New("force") + } + }, + }, + err: errors.New("error deleting ACME EAB Key Reference with Key ID keyID and reference ref"), + } + }, + "fail/delete-eak": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return errors.New("force") + } + }, + }, + err: errors.New("error deleting ACME EAB Key with Key ID keyID"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if err := db.DeleteExternalAccountKey(context.Background(), prov, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestDB_CreateExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + _id *string + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + var ( + id string + idPtr = &id + ) + now := clock.Now() + eak := &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + CreatedAt: now, + } + return test{ + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + + id = string(key) + + dbeak := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbeak)) + assert.Equals(t, string(key), dbeak.ID) + assert.Equals(t, eak.Provisioner, dbeak.Provisioner) + assert.Equals(t, eak.Reference, dbeak.Reference) + assert.Equals(t, 32, len(dbeak.KeyBytes)) + assert.False(t, dbeak.CreatedAt.IsZero()) + assert.Equals(t, dbeak.AccountID, eak.AccountID) + assert.True(t, dbeak.BoundAt.IsZero()) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + eak: eak, + _id: idPtr, + } + }, + "fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + err: errors.New("error saving acme external_account_key"), + } + }, + "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + err: errors.New("error saving acme external_account_key"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + eak, err := db.CreateExternalAccountKey(context.Background(), prov, ref) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, *tc._id, eak.ID) + assert.Equals(t, prov, eak.Provisioner) + assert.Equals(t, ref, eak.Reference) + assert.Equals(t, "", eak.AccountID) + assert.False(t, eak.CreatedAt.IsZero()) + assert.False(t, eak.AlreadyBound()) + assert.True(t, eak.BoundAt.IsZero()) + } + } + }) + } +} + +func TestDB_UpdateExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + type test struct { + db nosql.DB + eak *acme.ExternalAccountKey + err error + } + var tests = map[string]func(t *testing.T) test{ + + "ok": func(t *testing.T) test { + eak := &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + return test{ + eak: eak, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, old, b) + + dbNew := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbNew)) + assert.Equals(t, dbNew.ID, dbeak.ID) + assert.Equals(t, dbNew.Provisioner, dbeak.Provisioner) + assert.Equals(t, dbNew.Reference, dbeak.Reference) + assert.Equals(t, dbNew.AccountID, dbeak.AccountID) + assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) + assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) + assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) + return nu, true, nil + }, + }, + } + }, + "fail/provisioner-mismatch": func(t *testing.T) test { + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "differentProvisioner", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + }, + err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if err := db.UpdateExternalAccountKey(context.Background(), prov, tc.eak); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.eak.ID) + assert.Equals(t, dbeak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, dbeak.Reference, tc.eak.Reference) + assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) + assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) + } + } + }) + } +} diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 700881dc..a54243a4 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -137,13 +137,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) prov := chi.URLParam(r, "prov") reference := chi.URLParam(r, "ref") - // TODO: support paging properly? It'll probably leak to the DB layer, as we have to loop through all keys - // cursor, limit, err := api.ParseCursor(r) - // if err != nil { - // api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, - // "error parsing cursor and limit from query params")) - // return - // } + // TODO: support paging? It'll probably leak to the DB layer, as we have to loop through all keys var ( key *acme.ExternalAccountKey From 94f8e58bea175de1d6c76a2980d97a0c47f5346b Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 11 Oct 2021 23:03:30 +0200 Subject: [PATCH 24/47] Update go.step.sm/linkedca to v0.8.0 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0aab9189..35043e7e 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.6.0 go.step.sm/crypto v0.11.0 - go.step.sm/linkedca v0.5.0 + go.step.sm/linkedca v0.8.0 golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 golang.org/x/net v0.0.0-20210913180222-943fd674d43e google.golang.org/api v0.47.0 diff --git a/go.sum b/go.sum index 0c042a04..4b5e2929 100644 --- a/go.sum +++ b/go.sum @@ -541,6 +541,8 @@ go.step.sm/crypto v0.11.0 h1:VDpeVgEmqme/FK2w5QINxkOQ1FWOm/Wi2TwQXiacKr8= go.step.sm/crypto v0.11.0/go.mod h1:5YzQ85BujYBu6NH18jw7nFjwuRnDch35nLzH0ES5sKg= go.step.sm/linkedca v0.5.0 h1:oZVRSpElM7lAL1XN2YkjdHwI/oIZ+1ULOnuqYPM6xjY= go.step.sm/linkedca v0.5.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= +go.step.sm/linkedca v0.8.0 h1:86DAufqUtUvFTJgYpgG0McKkpqnjXxg53FTXYyhs0HI= +go.step.sm/linkedca v0.8.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= From a4660f73fa90475716aed0d43587a1d768e2a51e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 11 Oct 2021 23:10:16 +0200 Subject: [PATCH 25/47] Fix some of the gocritic remarks --- acme/db.go | 24 ++++++++++++------------ acme/db/nosql/account.go | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/acme/db.go b/acme/db.go index aa81f7d3..726aa937 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,11 +19,11 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error - CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) - GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) + GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) - GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) - DeleteExternalAccountKey(ctx context.Context, provisionerName string, keyID string) error + GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) + DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error CreateNonce(ctx context.Context) (Nonce, error) @@ -54,11 +54,11 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error - MockCreateExternalAccountKey func(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) - MockGetExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) + MockCreateExternalAccountKey func(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) + MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) - MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) - MockDeleteExternalAccountKey func(ctx context.Context, provisionerName string, keyID string) error + MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) + MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockCreateNonce func(ctx context.Context) (Nonce, error) @@ -125,7 +125,7 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error { } // CreateExternalAccountKey mock -func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName string, name string) (*ExternalAccountKey, error) { +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) { if m.MockCreateExternalAccountKey != nil { return m.MockCreateExternalAccountKey(ctx, provisionerName, name) } else if m.MockError != nil { @@ -135,7 +135,7 @@ func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName s } // GetExternalAccountKey mock -func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) { if m.MockGetExternalAccountKey != nil { return m.MockGetExternalAccountKey(ctx, provisionerName, keyID) } else if m.MockError != nil { @@ -155,7 +155,7 @@ func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName str } // GetExtrnalAccountKeyByReference mock -func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { if m.MockGetExternalAccountKeys != nil { return m.GetExternalAccountKeyByReference(ctx, provisionerName, reference) } else if m.MockError != nil { @@ -165,7 +165,7 @@ func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provision } // DeleteExternalAccountKey mock -func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerName string, keyID string) error { +func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error { if m.MockDeleteExternalAccountKey != nil { return m.MockDeleteExternalAccountKey(ctx, provisionerName, keyID) } else if m.MockError != nil { diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 2f698d87..cf05337c 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -170,7 +170,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { } // CreateExternalAccountKey creates a new External Account Binding key with a name -func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName string, reference string) (*acme.ExternalAccountKey, error) { +func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { keyID, err := randID() if err != nil { return nil, err @@ -216,7 +216,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName stri } // GetExternalAccountKey retrieves an External Account Binding key by KeyID -func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return nil, err @@ -237,7 +237,7 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName string, }, nil } -func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName string, keyID string) error { +func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error { dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) @@ -289,7 +289,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string } // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference -func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName string, reference string) (*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { if reference == "" { return nil, nil } From dd4b4b043556984c83e59ac5e2d9c3fcd4e554cc Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 11 Oct 2021 23:34:23 +0200 Subject: [PATCH 26/47] Fix remaining gocritic remarks --- acme/api/account_test.go | 22 +++--- acme/challenge.go | 7 +- acme/db/nosql/account.go | 4 +- acme/db/nosql/account_test.go | 144 ++++++++++++++++------------------ acme/order.go | 4 +- authority/admin/api/acme.go | 26 +++--- ca/adminClient.go | 6 +- 7 files changed, 103 insertions(+), 110 deletions(-) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 320e0ced..5b722cb9 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -69,12 +69,12 @@ const noKeyID = keyID("") // jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4. // Implementation taken from github.com/mholt/acmez -func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url string) ([]byte, error) { +func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, u string) ([]byte, error) { // §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm" alg, sha := "HS256", crypto.SHA256 // §7.3.4: "The 'nonce' field MUST NOT be present" - phead, err := jwsHead(alg, "", url, kid, nil) + phead, err := jwsHead(alg, "", u, kid, nil) if err != nil { return nil, err } @@ -99,7 +99,7 @@ func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url st // Since jwk and kid are mutually-exclusive, the jwk will be encoded // only if kid is empty. If nonce is empty, it will not be encoded. // Implementation taken from github.com/mholt/acmez -func jwsHead(alg, nonce, url string, kid keyID, key crypto.Signer) (string, error) { +func jwsHead(alg, nonce, u string, kid keyID, key crypto.Signer) (string, error) { phead := fmt.Sprintf(`{"alg":%q`, alg) if kid == noKeyID { jwk, err := jwkEncode(key.Public()) @@ -113,7 +113,7 @@ func jwsHead(alg, nonce, url string, kid keyID, key crypto.Signer) (string, erro if nonce != "" { phead += fmt.Sprintf(`,"nonce":%q`, nonce) } - phead += fmt.Sprintf(`,"url":%q}`, url) + phead += fmt.Sprintf(`,"url":%q}`, u) phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) return phead, nil } @@ -684,7 +684,7 @@ func TestHandler_NewAccount(t *testing.T) { assert.Equals(t, acc.Key, jwk) return nil }, - MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ ID: "eakID", Provisioner: escProvName, @@ -1058,7 +1058,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { createdAt := time.Now() return test{ db: &acme.MockDB{ - MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ ID: "eakID", Provisioner: escProvName, @@ -1091,7 +1091,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) - eab.Payload = eab.Payload + "{}" + eab.Payload += "{}" prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) @@ -1149,7 +1149,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ctx = context.WithValue(ctx, provisionerContextKey, prov) return test{ db: &acme.MockDB{ - MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return nil, acme.NewErrorISE("error retrieving external account key") }, }, @@ -1205,7 +1205,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { boundAt := time.Now().Add(1 * time.Second) return test{ db: &acme.MockDB{ - MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ ID: "eakID", Provisioner: escProvName, @@ -1240,7 +1240,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ctx = context.WithValue(ctx, provisionerContextKey, prov) return test{ db: &acme.MockDB{ - MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ ID: "eakID", Provisioner: escProvName, @@ -1276,7 +1276,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ctx = context.WithValue(ctx, provisionerContextKey, prov) return test{ db: &acme.MockDB{ - MockGetExternalAccountKey: func(ctx context.Context, provisionerName string, keyID string) (*acme.ExternalAccountKey, error) { + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ ID: "eakID", Provisioner: escProvName, diff --git a/acme/challenge.go b/acme/challenge.go index b880708c..24f8cc92 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -26,8 +26,11 @@ import ( type ChallengeType string const ( - HTTP01 ChallengeType = "http-01" - DNS01 ChallengeType = "dns-01" + // HTTP-01 challenge type + HTTP01 ChallengeType = "http-01" + // DNS-01 challenge type + DNS01 ChallengeType = "dns-01" + // TLS-ALPN-01 challenge type TLSALPN01 ChallengeType = "tls-alpn-01" ) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index cf05337c..c00185be 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -190,7 +190,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, ref CreatedAt: clock.Now(), } - if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { + if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { return nil, err } @@ -199,7 +199,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, ref Reference: dbeak.Reference, ExternalAccountKeyID: dbeak.ID, } - if err = db.save(ctx, dbeak.Reference, dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { + if err := db.save(ctx, dbeak.Reference, dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { return nil, err } } diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 78dc9a6a..b06ac6bb 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -771,8 +771,8 @@ func TestDB_getDBExternalAccountKey(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if dbeak, err := db.getDBExternalAccountKey(context.Background(), keyID); err != nil { + d := DB{db: tc.db} + if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -787,16 +787,14 @@ func TestDB_getDBExternalAccountKey(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, dbeak.ID, tc.dbeak.ID) - assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) - assert.Equals(t, dbeak.Provisioner, tc.dbeak.Provisioner) - assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) - assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) - assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) - assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.dbeak.ID) + assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) + assert.Equals(t, dbeak.Provisioner, tc.dbeak.Provisioner) + assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) + assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) + assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) + assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) } }) } @@ -890,8 +888,8 @@ func TestDB_GetExternalAccountKey(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if eak, err := db.GetExternalAccountKey(context.Background(), prov, keyID); err != nil { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKey(context.Background(), prov, keyID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -906,16 +904,14 @@ func TestDB_GetExternalAccountKey(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, eak.ID, tc.eak.ID) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) - assert.Equals(t, eak.Reference, tc.eak.Reference) - assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, eak.AccountID, tc.eak.AccountID) - assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.Reference, tc.eak.Reference) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) } }) } @@ -1056,8 +1052,8 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if eak, err := db.GetExternalAccountKeyByReference(context.Background(), prov, tc.ref); err != nil { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKeyByReference(context.Background(), prov, tc.ref); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1072,16 +1068,14 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && tc.eak != nil { - assert.Equals(t, eak.ID, tc.eak.ID) - assert.Equals(t, eak.AccountID, tc.eak.AccountID) - assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) - assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) - assert.Equals(t, eak.Reference, tc.eak.Reference) - } + } else if assert.Nil(t, tc.err) && tc.eak != nil { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.Reference, tc.eak.Reference) } }) } @@ -1208,8 +1202,8 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if eaks, err := db.GetExternalAccountKeys(context.Background(), prov); err != nil { + d := DB{db: tc.db} + if eaks, err := d.GetExternalAccountKeys(context.Background(), prov); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1224,18 +1218,16 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, len(eaks), len(tc.eaks)) - for i, eak := range eaks { - assert.Equals(t, eak.ID, tc.eaks[i].ID) - assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) - assert.Equals(t, eak.Provisioner, tc.eaks[i].Provisioner) - assert.Equals(t, eak.Reference, tc.eaks[i].Reference) - assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) - assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) - assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, len(eaks), len(tc.eaks)) + for i, eak := range eaks { + assert.Equals(t, eak.ID, tc.eaks[i].ID) + assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eaks[i].Provisioner) + assert.Equals(t, eak.Reference, tc.eaks[i].Reference) + assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) + assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) + assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) } } }) @@ -1440,8 +1432,8 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.DeleteExternalAccountKey(context.Background(), prov, keyID); err != nil { + d := DB{db: tc.db} + if err := d.DeleteExternalAccountKey(context.Background(), prov, keyID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1569,22 +1561,20 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - eak, err := db.CreateExternalAccountKey(context.Background(), prov, ref) + d := DB{db: tc.db} + eak, err := d.CreateExternalAccountKey(context.Background(), prov, ref) if err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, *tc._id, eak.ID) - assert.Equals(t, prov, eak.Provisioner) - assert.Equals(t, ref, eak.Reference) - assert.Equals(t, "", eak.AccountID) - assert.False(t, eak.CreatedAt.IsZero()) - assert.False(t, eak.AlreadyBound()) - assert.True(t, eak.BoundAt.IsZero()) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, *tc._id, eak.ID) + assert.Equals(t, prov, eak.Provisioner) + assert.Equals(t, ref, eak.Reference) + assert.Equals(t, "", eak.AccountID) + assert.False(t, eak.CreatedAt.IsZero()) + assert.False(t, eak.AlreadyBound()) + assert.True(t, eak.BoundAt.IsZero()) } }) } @@ -1649,7 +1639,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { } }, "fail/provisioner-mismatch": func(t *testing.T) test { - dbeak := &dbExternalAccountKey{ + newDBEAK := &dbExternalAccountKey{ ID: keyID, Provisioner: "differentProvisioner", Reference: ref, @@ -1657,7 +1647,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { KeyBytes: []byte{1, 3, 3, 7}, CreatedAt: now, } - b, err := json.Marshal(dbeak) + b, err := json.Marshal(newDBEAK) assert.FatalError(t, err) return test{ eak: &acme.ExternalAccountKey{ @@ -1694,21 +1684,19 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.UpdateExternalAccountKey(context.Background(), prov, tc.eak); err != nil { + d := DB{db: tc.db} + if err := d.UpdateExternalAccountKey(context.Background(), prov, tc.eak); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, dbeak.ID, tc.eak.ID) - assert.Equals(t, dbeak.Provisioner, tc.eak.Provisioner) - assert.Equals(t, dbeak.Reference, tc.eak.Reference) - assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) - assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) - assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.eak.ID) + assert.Equals(t, dbeak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, dbeak.Reference, tc.eak.Reference) + assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) + assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) } }) } diff --git a/acme/order.go b/acme/order.go index 237c6979..57e66f46 100644 --- a/acme/order.go +++ b/acme/order.go @@ -17,7 +17,9 @@ import ( type IdentifierType string const ( - IP IdentifierType = "ip" + // IP identifier type + IP IdentifierType = "ip" + // DNS identifier type DNS IdentifierType = "dns" ) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index a54243a4..9283dfc5 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -33,14 +33,14 @@ type GetExternalAccountKeysResponse struct { // before serving requests that act on ACME EAB credentials. func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - provisioner := chi.URLParam(r, "prov") - eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), provisioner) + prov := chi.URLParam(r, "prov") + eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), prov) if err != nil { api.WriteError(w, err) return } if !eabEnabled { - api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", provisioner)) + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov)) return } next(w, r) @@ -68,12 +68,12 @@ func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName return false, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID()) } - acme := details.GetACME() - if acme == nil { + acmeProvisioner := details.GetACME() + if acmeProvisioner == nil { return false, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) } - return acme.GetRequireEab(), nil + return acmeProvisioner.GetRequireEab(), nil } // CreateExternalAccountKey creates a new External Account Binding key @@ -89,23 +89,23 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques return } - provisioner := chi.URLParam(r, "prov") + prov := chi.URLParam(r, "prov") reference := body.Reference if reference != "" { - k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), provisioner, reference) + k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) // retrieving an EAB key from DB results in error if it doesn't exist, which is what we're looking for if err == nil || k != nil { - err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner %s with reference %s already exists", provisioner, reference) + err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner %s with reference %s already exists", prov, reference) err.Status = 409 api.WriteError(w, err) return } } - eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), provisioner, reference) + eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), prov, reference) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error creating ACME EAB key for provisioner %s and reference %s", provisioner, reference)) + api.WriteError(w, admin.WrapErrorISE(err, "error creating ACME EAB key for provisioner %s and reference %s", prov, reference)) return } @@ -121,10 +121,10 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques // DeleteExternalAccountKey deletes an ACME External Account Key. func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { - provisioner := chi.URLParam(r, "prov") + prov := chi.URLParam(r, "prov") keyID := chi.URLParam(r, "id") - if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), provisioner, keyID); err != nil { + if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), prov, keyID); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", keyID)) return } diff --git a/ca/adminClient.go b/ca/adminClient.go index 4bd994b5..b4c03c44 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -561,7 +561,7 @@ retry: } // GetExternalAccountKeysPaginate returns a page from the the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName string, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { +func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { var retried bool o := new(adminOptions) if err := o.apply(opts); err != nil { @@ -640,7 +640,7 @@ retry: } // RemoveExternalAccountKey performs the DELETE /admin/acme/eab/{prov}/{key_id} request to the CA. -func (c *AdminClient) RemoveExternalAccountKey(provisionerName string, keyID string) error { +func (c *AdminClient) RemoveExternalAccountKey(provisionerName, keyID string) error { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", provisionerName, "/", keyID)}) tok, err := c.generateAdminToken(u.Path) @@ -668,7 +668,7 @@ retry: } // GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. -func (c *AdminClient) GetExternalAccountKeys(provisionerName string, reference string, opts ...AdminOption) ([]*linkedca.EABKey, error) { +func (c *AdminClient) GetExternalAccountKeys(provisionerName, reference string, opts ...AdminOption) ([]*linkedca.EABKey, error) { var ( cursor = "" eaks = []*linkedca.EABKey{} From d354d55e7fc250508b50eb571923adf96e919f0d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 16 Oct 2021 14:44:56 +0200 Subject: [PATCH 27/47] Improve handling duplicate ACME EAB references --- acme/db.go | 2 +- acme/db/nosql/account.go | 2 +- acme/db/nosql/account_test.go | 2 +- authority/admin/api/acme.go | 13 +++++++++++-- authority/admin/errors.go | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/acme/db.go b/acme/db.go index 726aa937..a4173ce5 100644 --- a/acme/db.go +++ b/acme/db.go @@ -154,7 +154,7 @@ func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName str return m.MockRet1.([]*ExternalAccountKey), m.MockError } -// GetExtrnalAccountKeyByReference mock +// GetExternalAccountKeyByReference mock func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { if m.MockGetExternalAccountKeys != nil { return m.GetExternalAccountKeyByReference(ctx, provisionerName, reference) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index c00185be..aca85a76 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -295,7 +295,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerN } k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(reference)) if nosqlDB.IsErrNotFound(err) { - return nil, errors.Errorf("ACME EAB key for reference %s not found", reference) + return nil, acme.ErrNotFound } else if err != nil { return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) } diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index b06ac6bb..4b94e40f 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -992,7 +992,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { return nil, nosqldb.ErrNotFound }, }, - err: errors.New("ACME EAB key for reference ref not found"), + err: errors.New("not found"), } }, "fail/reference-load-error": func(t *testing.T) test { diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 9283dfc5..8f3cdf2b 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -92,15 +92,24 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques prov := chi.URLParam(r, "prov") reference := body.Reference + // check if a key with the reference does not exist (only when a reference was in the request) if reference != "" { k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) - // retrieving an EAB key from DB results in error if it doesn't exist, which is what we're looking for - if err == nil || k != nil { + // retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for, + // but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound. + shouldWriteError := err != nil && !(acme.ErrNotFound == err) + if shouldWriteError { + api.WriteError(w, err) + return + } + // if a key was found, return HTTP 409 conflict + if k != nil { err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner %s with reference %s already exists", prov, reference) err.Status = 409 api.WriteError(w, err) return } + // continue execution if no key was found for the reference } eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), prov, reference) diff --git a/authority/admin/errors.go b/authority/admin/errors.go index 607093b0..217227ca 100644 --- a/authority/admin/errors.go +++ b/authority/admin/errors.go @@ -104,7 +104,7 @@ var ( } ) -// Error represents an Admin +// Error represents an Admin error type Error struct { Type string `json:"type"` Detail string `json:"detail"` From bc5f0e429b4571422030b16a1e661b4d4c93d8d1 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sun, 17 Oct 2021 12:53:02 +0200 Subject: [PATCH 28/47] Fix gocritic remark --- authority/admin/api/acme.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 8f3cdf2b..6ca8deab 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -97,7 +97,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) // retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for, // but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound. - shouldWriteError := err != nil && !(acme.ErrNotFound == err) + shouldWriteError := err != nil && acme.ErrNotFound != err if shouldWriteError { api.WriteError(w, err) return From 4d726d6b4c88a4abd8232e3e9e0c3dcd6b788b39 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sun, 17 Oct 2021 22:42:36 +0200 Subject: [PATCH 29/47] Add pagination to ACME EAB credentials endpoint --- acme/db.go | 12 ++-- acme/db/nosql/account.go | 31 ++++++++-- acme/db/nosql/account_test.go | 112 ++++++++++++++++++++++++++++++++-- authority/admin/api/acme.go | 22 ++++--- 4 files changed, 154 insertions(+), 23 deletions(-) diff --git a/acme/db.go b/acme/db.go index a4173ce5..73029231 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,7 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) + GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error @@ -56,7 +56,7 @@ type MockDB struct { MockCreateExternalAccountKey func(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) + MockGetExternalAccountKeys func(ctx context.Context, provisionerName string, cursor string, limit int) ([]*ExternalAccountKey, string, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error @@ -145,13 +145,13 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, key } // GetExternalAccountKeys mock -func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) { if m.MockGetExternalAccountKeys != nil { - return m.MockGetExternalAccountKeys(ctx, provisionerName) + return m.MockGetExternalAccountKeys(ctx, provisionerName, 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/account.go b/acme/db/nosql/account.go index aca85a76..9464e9a4 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -259,21 +259,42 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, key } // GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner -func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { entries, err := db.db.List(externalAccountKeyTable) if err != nil { - return nil, err + return nil, "", err + } + + // set sane limits; based on the Admin API limits + switch { + case limit <= 0: + limit = 20 + case limit > 100: + limit = 100 } + foundCursorKey := false keys := []*acme.ExternalAccountKey{} - for _, entry := range entries { + for _, entry := range entries { // entries is sorted alphabetically on the key (ID) of the EAK; no need to sort this again. dbeak := new(dbExternalAccountKey) if err = json.Unmarshal(entry.Value, dbeak); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) + return nil, "", errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) } if dbeak.Provisioner != provisionerName { continue } + // skip the IDs not matching the cursor to look for in the sorted list. + if cursor != "" && !foundCursorKey && cursor != dbeak.ID { + continue + } + // look for the entry pointed to by the cursor (the next item to return), to start selecting items + if cursor != "" && !foundCursorKey && cursor == dbeak.ID { + foundCursorKey = true + } + // return if the limit of items was found in the previous iteration; the next cursor is set to the next item to return + if len(keys) == limit { + return keys, dbeak.ID, nil + } keys = append(keys, &acme.ExternalAccountKey{ ID: dbeak.ID, KeyBytes: dbeak.KeyBytes, @@ -285,7 +306,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string }) } - return keys, nil + return keys, "", nil } // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 4b94e40f..168e93c1 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -1085,13 +1085,17 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { keyID1 := "keyID1" keyID2 := "keyID2" keyID3 := "keyID3" + keyID4 := "keyID4" prov := "acmeProv" ref := "ref" type test struct { - db nosql.DB - err error - acmeErr *acme.Error - eaks []*acme.ExternalAccountKey + db nosql.DB + err error + cursor string + nextCursor string + limit int + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey } var tests = map[string]func(t *testing.T) test{ "ok": func(t *testing.T) test { @@ -1169,6 +1173,103 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { }, } }, + "ok/paging-single-entry": func(t *testing.T) test { + now := clock.Now() + dbeak1 := &dbExternalAccountKey{ + ID: keyID1, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b1, err := json.Marshal(dbeak1) + assert.FatalError(t, err) + dbeak2 := &dbExternalAccountKey{ + ID: keyID2, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b2, err := json.Marshal(dbeak2) + assert.FatalError(t, err) + dbeak3 := &dbExternalAccountKey{ + ID: keyID3, + Provisioner: "differentProvisioner", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b3, err := json.Marshal(dbeak3) + assert.FatalError(t, err) + dbeak4 := &dbExternalAccountKey{ + ID: keyID4, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b4, err := json.Marshal(dbeak4) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: b1, + }, + { + Bucket: bucket, + Key: []byte(keyID2), + Value: b2, + }, + { + Bucket: bucket, + Key: []byte(keyID3), + Value: b3, + }, + { + Bucket: bucket, + Key: []byte(keyID4), + Value: b4, + }, + }, nil + }, + }, + cursor: keyID2, + limit: 1, + nextCursor: keyID4, + eaks: []*acme.ExternalAccountKey{ + { + ID: keyID2, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + }, + } + }, + "ok/paging-max-limit": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + return []*nosqldb.Entry{}, nil + }, + }, + limit: 1337, + eaks: []*acme.ExternalAccountKey{}, + } + }, "fail/db.List-error": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ @@ -1203,7 +1304,7 @@ 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(), prov); err != nil { + if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), prov, tc.cursor, tc.limit); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1229,6 +1330,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) } + assert.Equals(t, nextCursor, tc.nextCursor) } }) } diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 6ca8deab..8cba39c4 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -146,13 +146,22 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) prov := chi.URLParam(r, "prov") reference := chi.URLParam(r, "ref") - // TODO: support paging? It'll probably leak to the DB layer, as we have to loop through all keys - var ( - key *acme.ExternalAccountKey - keys []*acme.ExternalAccountKey - err error + key *acme.ExternalAccountKey + keys []*acme.ExternalAccountKey + err error + cursor string + nextCursor string + limit int ) + + cursor, limit, err = api.ParseCursor(r) + if err != nil { + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, + "error parsing cursor and limit from query params")) + return + } + if reference != "" { key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) if err != nil { @@ -161,7 +170,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) } keys = []*acme.ExternalAccountKey{key} } else { - keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov) + keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit) if err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) return @@ -181,7 +190,6 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) } } - nextCursor := "" api.JSON(w, &GetExternalAccountKeysResponse{ EAKs: eaks, NextCursor: nextCursor, From 23898e9b76440c2024b0c4ae387dccdcc6b89d97 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 7 Dec 2021 12:17:41 +0100 Subject: [PATCH 30/47] Improve EAB JWS validation and increase test coverage --- acme/api/account.go | 99 ++++- acme/api/account_test.go | 848 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 905 insertions(+), 42 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index 12a3ea47..a917b0e0 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -9,8 +9,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/logging" - - squarejose "gopkg.in/square/go-jose.v2" + "go.step.sm/crypto/jose" ) // ExternalAccountBinding represents the ACME externalAccountBinding JWS @@ -94,12 +93,6 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } - eak, err := h.validateExternalAccountBinding(ctx, &nar) - if err != nil { - api.WriteError(w, err) - return - } - prov, err := acmeProvisionerFromContext(ctx) if err != nil { api.WriteError(w, err) @@ -107,7 +100,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { } httpStatus := http.StatusCreated - acc, err := accountFromContext(r.Context()) + acc, err := accountFromContext(ctx) if err != nil { acmeErr, ok := err.(*acme.Error) if !ok || acmeErr.Status != http.StatusBadRequest { @@ -122,12 +115,19 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { "account does not exist")) return } + jwk, err := jwkFromContext(ctx) if err != nil { api.WriteError(w, err) return } + eak, err := h.validateExternalAccountBinding(ctx, &nar) + if err != nil { + api.WriteError(w, err) + return + } + acc = &acme.Account{ Key: jwk, Contact: nar.Contact, @@ -137,6 +137,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { api.WriteError(w, acme.WrapErrorISE(err, "error creating account")) return } + if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response err := eak.BindTo(acc) if err != nil { @@ -256,26 +257,27 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") } - // TODO: extract the EAB in a similar manner as JWS, JWK, payload, etc? That would probably move a lot/all of - // the logic of this function into the middleware. Should not be too hard, because the middleware does know - // about the handler and thus about its dependencies. eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) if err != nil { return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes") } - eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) + eabJWS, err := jose.ParseJWS(string(eabJSONBytes)) if err != nil { return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") } - // TODO: implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration + // TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration? + + keyID, acmeErr := validateEABJWS(ctx, eabJWS) + if acmeErr != nil { + return nil, acmeErr + } - keyID := eabJWS.Signatures[0].Protected.KeyID externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.Name, keyID) if err != nil { if _, ok := err.(*acme.Error); ok { - return nil, err + return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") } return nil, acme.WrapErrorISE(err, "error retrieving external account key") } @@ -294,14 +296,14 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, err } - var payloadJWK *squarejose.JSONWebKey + var payloadJWK *jose.JSONWebKey err = json.Unmarshal(payload, &payloadJWK) if err != nil { return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") } if !keysAreEqual(jwk, payloadJWK) { - return nil, acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match") // TODO: decide ACME error type to use + return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match") } return externalAccountKey, nil @@ -309,7 +311,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc // keysAreEqual performs an equality check on two JWKs by comparing // the (base64 encoding) of the Key IDs. -func keysAreEqual(x, y *squarejose.JSONWebKey) bool { +func keysAreEqual(x, y *jose.JSONWebKey) bool { if x == nil || y == nil { return false } @@ -320,3 +322,62 @@ func keysAreEqual(x, y *squarejose.JSONWebKey) bool { } return digestX == digestY } + +// validateEABJWS verifies the contents of the External Account Binding JWS. +// The protected header of the JWS MUST meet the following criteria: +// o The "alg" field MUST indicate a MAC-based algorithm +// o The "kid" field MUST contain the key identifier provided by the CA +// o The "nonce" field MUST NOT be present +// o The "url" field MUST be set to the same value as the outer JWS +func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) { + + if jws == nil { + return "", acme.NewErrorISE("no JWS provided") + } + + if len(jws.Signatures) != 1 { + return "", acme.NewError(acme.ErrorMalformedType, "JWS must have one signature") + } + + header := jws.Signatures[0].Protected + algorithm := header.Algorithm + keyID := header.KeyID + nonce := header.Nonce + + if !(algorithm == jose.HS256 || algorithm == jose.HS384 || algorithm == jose.HS512) { + return "", acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm '%s'", algorithm) + } + + if keyID == "" { + return "", acme.NewError(acme.ErrorMalformedType, "'kid' field is required") + } + + if nonce != "" { + return "", acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present") + } + + jwsURL, ok := header.ExtraHeaders["url"] + if !ok { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field is required") + } + + outerJWS, err := jwsFromContext(ctx) + if err != nil { + return "", acme.WrapErrorISE(err, "could not retrieve outer JWS from context") + } + + if len(outerJWS.Signatures) != 1 { + return "", acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature") + } + + outerJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders["url"] + if !ok { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS") + } + + if jwsURL != outerJWSURL { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS") + } + + return keyID, nil +} diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 3af97a73..16d43c68 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -11,9 +11,8 @@ import ( "encoding/base64" "encoding/json" "fmt" - "math/big" - "io" + "math/big" "net/http/httptest" "net/url" "testing" @@ -25,7 +24,6 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" - squarejose "gopkg.in/square/go-jose.v2" ) var ( @@ -496,7 +494,8 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, statusCode: 500, @@ -509,7 +508,8 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, nil) return test{ ctx: ctx, @@ -547,6 +547,7 @@ func TestHandler_NewAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, provisionerContextKey, prov) ctx = context.WithValue(ctx, jwkContextKey, jwk) return test{ db: &acme.MockDB{ @@ -561,6 +562,108 @@ func TestHandler_NewAccount(t *testing.T) { err: acme.NewErrorISE("force"), } }, + "fail/acmeProvisionerFromContext": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + scepProvisioner := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + if err := scepProvisioner.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil { + assert.FatalError(t, err) + } + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + return test{ + ctx: ctx, + statusCode: 500, + err: acme.NewError(acme.ErrorServerInternalType, "provisioner in context is not an ACME provisioner"), + } + }, + "fail/db.UpdateExternalAccountKey-error": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + eak := &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + } + return test{ + db: &acme.MockDB{ + MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" + assert.Equals(t, acc.Contact, nar.Contact) + assert.Equals(t, acc.Key, jwk) + return nil + }, + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return eak, nil + }, + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { + return errors.New("force") + }, + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), + ExternalAccountBinding: eab, + }, + ctx: ctx, + statusCode: 500, + err: acme.NewError(acme.ErrorServerInternalType, "error updating external account binding key"), + } + }, "ok/new-account": func(t *testing.T) test { nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -660,7 +763,8 @@ func TestHandler_NewAccount(t *testing.T) { "ok/new-account-with-eab": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) @@ -669,14 +773,29 @@ func TestHandler_NewAccount(t *testing.T) { Contact: []string{"foo", "bar"}, ExternalAccountBinding: eab, } - b, err := json.Marshal(nar) + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { @@ -958,8 +1077,8 @@ func Test_keysAreEqual(t *testing.T) { assert.FatalError(t, err) wrongJWK.Key = struct{}{} type args struct { - x *squarejose.JSONWebKey - y *squarejose.JSONWebKey + x *jose.JSONWebKey + y *jose.JSONWebKey } tests := []struct { name string @@ -1046,16 +1165,38 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { "ok/eab": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) createdAt := time.Now() return test{ db: &acme.MockDB{ @@ -1084,6 +1225,36 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { err: nil, } }, + "fail/acmeProvisionerFromContext": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + scepProvisioner := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + if err := scepProvisioner.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil { + assert.FatalError(t, err) + } + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + return test{ + ctx: ctx, + err: acme.NewError(acme.ErrorServerInternalType, "could not load ACME provisioner from context: provisioner in context is not an ACME provisioner"), + } + }, "fail/parse-eab-jose": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) @@ -1109,19 +1280,88 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { err: acme.NewErrorISE("error parsing externalAccountBinding jws"), } }, + "fail/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) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + parsedJWS.Signatures = []jose.Signature{} + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{}, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature"), + } + }, "fail/retrieve-eab-key-db-failure": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockError: errors.New("db failure"), @@ -1135,23 +1375,45 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { err: acme.NewErrorISE("error retrieving external account key"), } }, - "fail/retrieve-eab-key-not-found": func(t *testing.T) test { + "fail/db.GetExternalAccountKey-not-found": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "unknown-key-id", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { - return nil, acme.NewErrorISE("error retrieving external account key") + return nil, acme.ErrNotFound }, }, ctx: ctx, @@ -1160,22 +1422,94 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewErrorISE("error loading external account key unknown-key-id"), + err: acme.NewErrorISE("error retrieving external account key"), } }, - "fail/retrieve-eab-wrong-provisioner": func(t *testing.T) test { + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return nil, errors.New("force") + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error retrieving external account key"), + } + }, + "fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockError: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), @@ -1186,22 +1520,44 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), + err: acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key: name of provisioner does not match provisioner for which the EAB key was created"), } }, "fail/eab-already-bound": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) createdAt := time.Now() boundAt := time.Now().Add(1 * time.Second) return test{ @@ -1229,16 +1585,38 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { "fail/eab-verify": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { @@ -1265,16 +1643,38 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { assert.FatalError(t, err) differentJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(differentJWK.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(differentJWK.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} err = json.Unmarshal(eabJWS, &eab) assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) prov := newACMEProv(t) prov.RequireEAB = true ctx := context.WithValue(context.Background(), jwkContextKey, jwk) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) return test{ db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { @@ -1293,7 +1693,118 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewError(acme.ErrorMalformedType, "keys in jws and eab payload do not match"), + err: acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match"), + } + }, + "fail/no-jwk": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorServerInternalType, "jwk expected in request context"), + } + }, + "fail/nil-jwk": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, nil) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: escProvName, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorServerInternalType, "jwk expected in request context"), } }, } @@ -1314,6 +1825,8 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { assert.Type(t, &acme.Error{}, err) ae, _ := err.(*acme.Error) assert.Equals(t, ae.Type, tc.err.Type) + assert.Equals(t, ae.Status, tc.err.Status) + assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error()) assert.Equals(t, ae.Detail, tc.err.Detail) assert.Equals(t, ae.Identifier, tc.err.Identifier) assert.Equals(t, ae.Subproblems, tc.err.Subproblems) @@ -1334,3 +1847,292 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { }) } } + +func Test_validateEABJWS(t *testing.T) { + acmeProv := newACMEProv(t) + escProvName := url.PathEscape(acmeProv.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + type test struct { + ctx context.Context + jws *jose.JSONWebSignature + keyID string + err *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/nil-jws": func(t *testing.T) test { + return test{ + jws: nil, + err: acme.NewErrorISE("no JWS provided"), + } + }, + "fail/invalid-number-of-signatures": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures = append(parsedEABJWS.Signatures, jose.Signature{}) + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "JWS must have one signature"), + } + }, + "fail/invalid-algorithm": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures[0].Protected.Algorithm = "HS42" + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm 'HS42'"), + } + }, + "fail/kid-not-set": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures[0].Protected.KeyID = "" + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'kid' field is required"), + } + }, + "fail/nonce-not-empty": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + parsedEABJWS.Signatures[0].Protected.Nonce = "some-bogus-nonce" + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present"), + } + }, + "fail/url-not-set": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + delete(parsedEABJWS.Signatures[0].Protected.ExtraHeaders, "url") + return test{ + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field is required"), + } + }, + "fail/no-outer-jws": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, nil) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewErrorISE("could not retrieve outer JWS from context"), + } + }, + "fail/outer-jws-multiple-signatures": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + outerJWS.Signatures = append(outerJWS.Signatures, jose.Signature{}) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature"), + } + }, + "fail/outer-jws-no-url": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS"), + } + }, + "fail/outer-jws-with-different-url": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", "this-is-not-the-same-url-as-in-the-eab-jws") + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS"), + } + }, + "ok": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(eabJWS, &eab) + assert.FatalError(t, err) + parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: parsedEABJWS, + keyID: "eakID", + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + keyID, err := validateEABJWS(tc.ctx, tc.jws) + wantErr := tc.err != nil + gotErr := err != nil + if wantErr != gotErr { + t.Errorf("validateEABJWS() error = %v, want %v", err, tc.err) + } + if wantErr { + assert.NotNil(t, err) + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Status, err.Status) + assert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error()) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, tc.err.Identifier, err.Identifier) + assert.Equals(t, tc.err.Subproblems, err.Subproblems) + } else { + assert.Nil(t, err) + assert.Equals(t, tc.keyID, keyID) + } + }) + } +} From 6e116572046f3f4efff7b37e1526d88969d2017a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 7 Dec 2021 14:57:39 +0100 Subject: [PATCH 31/47] Refactor creation of (raw) EAB JWS contents --- acme/api/account_test.go | 331 ++++++++++++++------------------------- 1 file changed, 119 insertions(+), 212 deletions(-) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 16d43c68..cd1d8a4f 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -3,16 +3,9 @@ package api import ( "bytes" "context" - "crypto" - "crypto/ecdsa" - "crypto/hmac" - "crypto/rsa" - "crypto/sha256" - "encoding/base64" "encoding/json" "fmt" "io" - "math/big" "net/http/httptest" "net/url" "testing" @@ -57,125 +50,55 @@ func newACMEProv(t *testing.T) *provisioner.ACME { return a } -var errUnsupportedKey = fmt.Errorf("unknown key type; only RSA and ECDSA are supported") - -// keyID is the account identity provided by a CA during registration. -type keyID string - -// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID. -// See jwsEncodeJSON for details. -const noKeyID = keyID("") - -// jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4. -// Implementation taken from github.com/mholt/acmez -func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, u string) ([]byte, error) { - // §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm" - alg, sha := "HS256", crypto.SHA256 - - // §7.3.4: "The 'nonce' field MUST NOT be present" - phead, err := jwsHead(alg, "", u, kid, nil) +func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID string, url string) (*jose.JSONWebSignature, error) { + signer, err := jose.NewSigner( + jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm("HS256"), + Key: hmacKey, + }, + &jose.SignerOptions{ + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": keyID, + "url": url, + }, + EmbedJWK: false, + }, + ) if err != nil { return nil, err } - encodedKey, err := jwkEncode(accountKey) + jwkJSONBytes, err := jwk.Public().MarshalJSON() if err != nil { return nil, err } - payload := base64.RawURLEncoding.EncodeToString([]byte(encodedKey)) - - payloadToSign := []byte(phead + "." + payload) - - h := hmac.New(sha256.New, hmacKey) - h.Write(payloadToSign) - sig := h.Sum(nil) - - return jwsFinal(sha, sig, phead, payload) -} - -// jwsHead constructs the protected JWS header for the given fields. -// Since jwk and kid are mutually-exclusive, the jwk will be encoded -// only if kid is empty. If nonce is empty, it will not be encoded. -// Implementation taken from github.com/mholt/acmez -func jwsHead(alg, nonce, u string, kid keyID, key crypto.Signer) (string, error) { - phead := fmt.Sprintf(`{"alg":%q`, alg) - if kid == noKeyID { - jwk, err := jwkEncode(key.Public()) - if err != nil { - return "", err - } - phead += fmt.Sprintf(`,"jwk":%s`, jwk) - } else { - phead += fmt.Sprintf(`,"kid":%q`, kid) + jws, err := signer.Sign(jwkJSONBytes) + if err != nil { + return nil, err } - if nonce != "" { - phead += fmt.Sprintf(`,"nonce":%q`, nonce) + + raw, err := jws.CompactSerialize() + if err != nil { + return nil, err } - phead += fmt.Sprintf(`,"url":%q}`, u) - phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) - return phead, nil -} -// jwkEncode encodes public part of an RSA or ECDSA key into a JWK. -// The result is also suitable for creating a JWK thumbprint. -// https://tools.ietf.org/html/rfc7517 -// Implementation taken from github.com/mholt/acmez -func jwkEncode(pub crypto.PublicKey) (string, error) { - switch pub := pub.(type) { - case *rsa.PublicKey: - // https://tools.ietf.org/html/rfc7518#section-6.3.1 - n := pub.N - e := big.NewInt(int64(pub.E)) - // Field order is important. - // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. - return fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, - base64.RawURLEncoding.EncodeToString(e.Bytes()), - base64.RawURLEncoding.EncodeToString(n.Bytes()), - ), nil - case *ecdsa.PublicKey: - // https://tools.ietf.org/html/rfc7518#section-6.2.1 - p := pub.Curve.Params() - n := p.BitSize / 8 - if p.BitSize%8 != 0 { - n++ - } - x := pub.X.Bytes() - if n > len(x) { - x = append(make([]byte, n-len(x)), x...) - } - y := pub.Y.Bytes() - if n > len(y) { - y = append(make([]byte, n-len(y)), y...) - } - // Field order is important. - // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. - return fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, - p.Name, - base64.RawURLEncoding.EncodeToString(x), - base64.RawURLEncoding.EncodeToString(y), - ), nil + parsedJWS, err := jose.ParseJWS(raw) + if err != nil { + return nil, err } - return "", errUnsupportedKey + + return parsedJWS, nil } -// jwsFinal constructs the final JWS object. -// Implementation taken from github.com/mholt/acmez -func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) { - enc := struct { - Protected string `json:"protected"` - Payload string `json:"payload"` - Sig string `json:"signature"` - }{ - Protected: phead, - Payload: payload, - Sig: base64.RawURLEncoding.EncodeToString(sig), - } - result, err := json.Marshal(&enc) +func createRawEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID string, url string) ([]byte, error) { + jws, err := createEABJWS(jwk, hmacKey, keyID, url) if err != nil { return nil, err } - return result, nil + + rawJWS := jws.FullSerialize() + return []byte(rawJWS), nil } func TestNewAccountRequest_Validate(t *testing.T) { @@ -565,10 +488,11 @@ func TestHandler_NewAccount(t *testing.T) { "fail/acmeProvisionerFromContext": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + 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(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -597,10 +521,10 @@ func TestHandler_NewAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -723,10 +647,11 @@ func TestHandler_NewAccount(t *testing.T) { "ok/new-account-no-eab-required": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -764,10 +689,10 @@ func TestHandler_NewAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1142,10 +1067,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { "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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) prov := newACMEProv(t) ctx := context.WithValue(context.Background(), jwkContextKey, jwk) @@ -1166,10 +1092,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1228,10 +1154,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { "fail/acmeProvisionerFromContext": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + 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(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1258,10 +1185,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { "fail/parse-eab-jose": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) eab.Payload += "{}" prov := newACMEProv(t) @@ -1284,10 +1212,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1331,10 +1259,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1379,10 +1307,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1429,10 +1357,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1479,10 +1407,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1527,10 +1455,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1586,10 +1514,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1644,10 +1572,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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) - eabJWS, err := jwsEncodeEAB(differentJWK.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(differentJWK, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1700,10 +1628,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1755,10 +1683,10 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + err = json.Unmarshal(rawEABJWS, &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1868,109 +1796,86 @@ func Test_validateEABJWS(t *testing.T) { "fail/invalid-number-of-signatures": func(t *testing.T) test { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) - assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + 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) - parsedEABJWS.Signatures = append(parsedEABJWS.Signatures, jose.Signature{}) + eabJWS.Signatures = append(eabJWS.Signatures, jose.Signature{}) return test{ - jws: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) - assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + 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) - parsedEABJWS.Signatures[0].Protected.Algorithm = "HS42" + eabJWS.Signatures[0].Protected.Algorithm = "HS42" return test{ - jws: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) - assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + 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) - parsedEABJWS.Signatures[0].Protected.KeyID = "" + eabJWS.Signatures[0].Protected.KeyID = "" return test{ - jws: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) - assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + 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) - parsedEABJWS.Signatures[0].Protected.Nonce = "some-bogus-nonce" + eabJWS.Signatures[0].Protected.Nonce = "some-bogus-nonce" return test{ - jws: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) - assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + 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(parsedEABJWS.Signatures[0].Protected.ExtraHeaders, "url") + delete(eabJWS.Signatures[0].Protected.ExtraHeaders, "url") return test{ - jws: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) - assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) - assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + 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: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + rawEABJWS := eabJWS.FullSerialize() assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -1995,19 +1900,20 @@ func Test_validateEABJWS(t *testing.T) { ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) return test{ ctx: ctx, - jws: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + rawEABJWS := eabJWS.FullSerialize() assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -2031,19 +1937,20 @@ func Test_validateEABJWS(t *testing.T) { ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) return test{ ctx: ctx, - jws: parsedEABJWS, + 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) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName)) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + rawEABJWS := eabJWS.FullSerialize() assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -2068,7 +1975,7 @@ func Test_validateEABJWS(t *testing.T) { ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) return test{ ctx: ctx, - jws: parsedEABJWS, + jws: eabJWS, err: acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS"), } }, @@ -2076,12 +1983,12 @@ func Test_validateEABJWS(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) - eabJWS, err := jwsEncodeEAB(jwk.Public().Key, []byte{1, 3, 3, 7}, "eakID", url) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) assert.FatalError(t, err) - eab := &ExternalAccountBinding{} - err = json.Unmarshal(eabJWS, &eab) + rawEABJWS := eabJWS.FullSerialize() assert.FatalError(t, err) - parsedEABJWS, err := jose.ParseJWS(string(eabJWS)) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) assert.FatalError(t, err) nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -2106,7 +2013,7 @@ func Test_validateEABJWS(t *testing.T) { ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) return test{ ctx: ctx, - jws: parsedEABJWS, + jws: eabJWS, keyID: "eakID", err: nil, } From 9885d42711117756a504abda55e86c5aa4773bbd Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 7 Dec 2021 16:28:06 +0100 Subject: [PATCH 32/47] Fix linting issues --- acme/api/account_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/acme/api/account_test.go b/acme/api/account_test.go index cd1d8a4f..aa8d44ba 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -50,7 +50,7 @@ func newACMEProv(t *testing.T) *provisioner.ACME { return a } -func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID string, url string) (*jose.JSONWebSignature, error) { +func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) (*jose.JSONWebSignature, error) { signer, err := jose.NewSigner( jose.SigningKey{ Algorithm: jose.SignatureAlgorithm("HS256"), @@ -59,7 +59,7 @@ func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID string, url string &jose.SignerOptions{ ExtraHeaders: map[jose.HeaderKey]interface{}{ "kid": keyID, - "url": url, + "url": u, }, EmbedJWK: false, }, @@ -91,8 +91,8 @@ func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID string, url string return parsedJWS, nil } -func createRawEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID string, url string) ([]byte, error) { - jws, err := createEABJWS(jwk, hmacKey, keyID, url) +func createRawEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) ([]byte, error) { + jws, err := createEABJWS(jwk, hmacKey, keyID, u) if err != nil { return nil, err } From 2215a05c280e6e166dd6cbb68ab4b78c9d62b17f Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 8 Dec 2021 15:19:38 +0100 Subject: [PATCH 33/47] Add tests for ACME EAB Admin Refactored some of the existing bits for testing the Authority API by creation of a new LinkedAuthority interface and changing visibility of the MockAuthority to be usable by other packages. At this time, not all of the functions of MockAuthority it usable yet. Will refactor when needed or requested. --- acme/api/handler_test.go | 86 +++++-- api/api.go | 311 +++++++++++++++++++++++ api/api_test.go | 230 +---------------- api/revoke_test.go | 8 +- api/ssh_test.go | 14 +- authority/admin/api/acme.go | 2 +- authority/admin/api/acme_test.go | 416 +++++++++++++++++++++++++++++++ authority/admin/api/handler.go | 7 +- go.mod | 1 + 9 files changed, 825 insertions(+), 250 deletions(-) create mode 100644 authority/admin/api/acme_test.go diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index 14e00f12..67f7df30 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -15,9 +15,11 @@ import ( "time" "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" ) @@ -51,28 +53,76 @@ func TestHandler_GetNonce(t *testing.T) { func TestHandler_GetDirectory(t *testing.T) { linker := NewLinker("ca.smallstep.com", "acme") - - prov := newProv() - provName := url.PathEscape(prov.GetName()) - baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - - expDir := Directory{ - NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), - NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), - NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName), - RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName), - KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName), - } - type test struct { + ctx context.Context statusCode int + dir Directory err *acme.Error } var tests = map[string]func(t *testing.T) test{ + "fail/no-provisioner": func(t *testing.T) test { + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, nil) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + return test{ + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"), + } + }, + "fail/different-provisioner": func(t *testing.T) test { + prov := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + return test{ + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"), + } + }, "ok": func(t *testing.T) test { + prov := newProv() + provName := url.PathEscape(prov.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + expDir := Directory{ + NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), + NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), + NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName), + RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName), + KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName), + } return test{ + ctx: ctx, + dir: expDir, + statusCode: 200, + } + }, + "ok/eab-required": func(t *testing.T) test { + prov := newACMEProv(t) + prov.RequireEAB = true + provName := url.PathEscape(prov.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + expDir := Directory{ + NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), + NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), + NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName), + RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName), + KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName), + Meta: Meta{ + ExternalAccountRequired: true, + }, + } + return test{ + ctx: ctx, + dir: expDir, statusCode: 200, } }, @@ -82,7 +132,7 @@ func TestHandler_GetDirectory(t *testing.T) { t.Run(name, func(t *testing.T) { h := &Handler{linker: linker} req := httptest.NewRequest("GET", "/foo/bar", nil) - req = req.WithContext(ctx) + req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.GetDirectory(w, req) res := w.Result() @@ -105,7 +155,9 @@ func TestHandler_GetDirectory(t *testing.T) { } else { var dir Directory json.Unmarshal(bytes.TrimSpace(body), &dir) - assert.Equals(t, dir, expDir) + if !cmp.Equal(tc.dir, dir) { + t.Errorf("GetDirectory() diff =\n%s", cmp.Diff(tc.dir, dir)) + } assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) diff --git a/api/api.go b/api/api.go index e057caaa..468870b6 100644 --- a/api/api.go +++ b/api/api.go @@ -25,6 +25,9 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" + "github.com/smallstep/certificates/templates" + "go.step.sm/linkedca" + "golang.org/x/crypto/ssh" ) // Authority is the interface implemented by a CA authority. @@ -48,6 +51,21 @@ type Authority interface { Version() authority.Version } +type LinkedAuthority interface { // TODO(hs): name is not great; it is related to LinkedCA, though + Authority + IsAdminAPIEnabled() bool + LoadAdminByID(id string) (*linkedca.Admin, bool) + GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) + StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error + UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) + RemoveAdmin(ctx context.Context, id string) error + AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) + StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error + LoadProvisionerByID(id string) (provisioner.Interface, error) + UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error + RemoveProvisioner(ctx context.Context, id string) error +} + // TimeDuration is an alias of provisioner.TimeDuration type TimeDuration = provisioner.TimeDuration @@ -457,3 +475,296 @@ func fmtPublicKey(cert *x509.Certificate) string { } return fmt.Sprintf("%s %s", cert.PublicKeyAlgorithm, params) } + +type MockAuthority struct { + ret1, ret2 interface{} + err error + authorizeSign func(ott string) ([]provisioner.SignOption, error) + getTLSOptions func() *authority.TLSOptions + root func(shasum string) (*x509.Certificate, error) + sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) + renew func(cert *x509.Certificate) ([]*x509.Certificate, error) + rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) + loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) + MockLoadProvisionerByName func(name string) (provisioner.Interface, error) + getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) + revoke func(context.Context, *authority.RevokeOptions) error + getEncryptedKey func(kid string) (string, error) + getRoots func() ([]*x509.Certificate, error) + getFederation func() ([]*x509.Certificate, error) + signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) + renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) + rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) + getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error) + getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error) + getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) + checkSSHHost func(ctx context.Context, principal, token string) (bool, error) + getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error) + version func() authority.Version + + MockRet1, MockRet2 interface{} // TODO: refactor the ret1/ret2 into those two + MockErr error + MockIsAdminAPIEnabled func() bool + MockLoadAdminByID func(id string) (*linkedca.Admin, bool) + MockGetAdmins func(cursor string, limit int) ([]*linkedca.Admin, string, error) + MockStoreAdmin func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error + MockUpdateAdmin func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) + MockRemoveAdmin func(ctx context.Context, id string) error + MockAuthorizeAdminToken func(r *http.Request, token string) (*linkedca.Admin, error) + MockStoreProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error + MockLoadProvisionerByID func(id string) (provisioner.Interface, error) + MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error + MockRemoveProvisioner func(ctx context.Context, id string) error +} + +// TODO: remove once Authorize is deprecated. +func (m *MockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { + return m.AuthorizeSign(ott) +} + +func (m *MockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { + if m.authorizeSign != nil { + return m.authorizeSign(ott) + } + return m.ret1.([]provisioner.SignOption), m.err +} + +func (m *MockAuthority) GetTLSOptions() *authority.TLSOptions { + if m.getTLSOptions != nil { + return m.getTLSOptions() + } + return m.ret1.(*authority.TLSOptions) +} + +func (m *MockAuthority) Root(shasum string) (*x509.Certificate, error) { + if m.root != nil { + return m.root(shasum) + } + return m.ret1.(*x509.Certificate), m.err +} + +func (m *MockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + if m.sign != nil { + return m.sign(cr, opts, signOpts...) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *MockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { + if m.renew != nil { + return m.renew(cert) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *MockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { + if m.rekey != nil { + return m.rekey(oldcert, pk) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *MockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { + if m.getProvisioners != nil { + return m.getProvisioners(nextCursor, limit) + } + return m.ret1.(provisioner.List), m.ret2.(string), m.err +} + +func (m *MockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) { + if m.loadProvisionerByCertificate != nil { + return m.loadProvisionerByCertificate(cert) + } + return m.ret1.(provisioner.Interface), m.err +} + +func (m *MockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { + if m.MockLoadProvisionerByName != nil { + return m.MockLoadProvisionerByName(name) + } + return m.ret1.(provisioner.Interface), m.err +} + +func (m *MockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { + if m.revoke != nil { + return m.revoke(ctx, opts) + } + return m.err +} + +func (m *MockAuthority) GetEncryptedKey(kid string) (string, error) { + if m.getEncryptedKey != nil { + return m.getEncryptedKey(kid) + } + return m.ret1.(string), m.err +} + +func (m *MockAuthority) GetRoots() ([]*x509.Certificate, error) { + if m.getRoots != nil { + return m.getRoots() + } + return m.ret1.([]*x509.Certificate), m.err +} + +func (m *MockAuthority) GetFederation() ([]*x509.Certificate, error) { + if m.getFederation != nil { + return m.getFederation() + } + return m.ret1.([]*x509.Certificate), m.err +} + +func (m *MockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + if m.signSSH != nil { + return m.signSSH(ctx, key, opts, signOpts...) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *MockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) { + if m.signSSHAddUser != nil { + return m.signSSHAddUser(ctx, key, cert) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *MockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) { + if m.renewSSH != nil { + return m.renewSSH(ctx, cert) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *MockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + if m.rekeySSH != nil { + return m.rekeySSH(ctx, cert, key, signOpts...) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *MockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) { + if m.getSSHHosts != nil { + return m.getSSHHosts(ctx, cert) + } + return m.ret1.([]authority.Host), m.err +} + +func (m *MockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) { + if m.getSSHRoots != nil { + return m.getSSHRoots(ctx) + } + return m.ret1.(*authority.SSHKeys), m.err +} + +func (m *MockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) { + if m.getSSHFederation != nil { + return m.getSSHFederation(ctx) + } + return m.ret1.(*authority.SSHKeys), m.err +} + +func (m *MockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { + if m.getSSHConfig != nil { + return m.getSSHConfig(ctx, typ, data) + } + return m.ret1.([]templates.Output), m.err +} + +func (m *MockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { + if m.checkSSHHost != nil { + return m.checkSSHHost(ctx, principal, token) + } + return m.ret1.(bool), m.err +} + +func (m *MockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) { + if m.getSSHBastion != nil { + return m.getSSHBastion(ctx, user, hostname) + } + return m.ret1.(*authority.Bastion), m.err +} + +func (m *MockAuthority) Version() authority.Version { + if m.version != nil { + return m.version() + } + return m.ret1.(authority.Version) +} + +func (m *MockAuthority) IsAdminAPIEnabled() bool { + if m.MockIsAdminAPIEnabled != nil { + return m.MockIsAdminAPIEnabled() + } + return m.MockRet1.(bool) +} + +func (m *MockAuthority) LoadAdminByID(id string) (*linkedca.Admin, bool) { + if m.MockLoadAdminByID != nil { + return m.MockLoadAdminByID(id) + } + return m.MockRet1.(*linkedca.Admin), m.MockRet2.(bool) +} + +func (m *MockAuthority) GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) { + if m.MockGetAdmins != nil { + return m.MockGetAdmins(cursor, limit) + } + return m.MockRet1.([]*linkedca.Admin), m.MockRet2.(string), m.MockErr +} + +func (m *MockAuthority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + if m.MockStoreAdmin != nil { + return m.MockStoreAdmin(ctx, adm, prov) + } + return m.MockErr +} + +func (m *MockAuthority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + if m.MockUpdateAdmin != nil { + return m.MockUpdateAdmin(ctx, id, nu) + } + return m.MockRet1.(*linkedca.Admin), m.MockErr +} + +func (m *MockAuthority) RemoveAdmin(ctx context.Context, id string) error { + if m.MockRemoveAdmin != nil { + return m.MockRemoveAdmin(ctx, id) + } + return m.MockErr +} + +func (m *MockAuthority) AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) { + if m.MockAuthorizeAdminToken != nil { + return m.MockAuthorizeAdminToken(r, token) + } + return m.MockRet1.(*linkedca.Admin), m.MockErr +} + +func (m *MockAuthority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { + if m.MockStoreProvisioner != nil { + return m.MockStoreProvisioner(ctx, prov) + } + return m.MockErr +} + +func (m *MockAuthority) LoadProvisionerByID(id string) (provisioner.Interface, error) { + if m.MockLoadProvisionerByID != nil { + return m.MockLoadProvisionerByID(id) + } + return m.MockRet1.(provisioner.Interface), m.MockErr +} + +func (m *MockAuthority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error { + if m.MockUpdateProvisioner != nil { + return m.MockUpdateProvisioner(ctx, nu) + } + return m.MockErr +} + +func (m *MockAuthority) RemoveProvisioner(ctx context.Context, id string) error { + if m.MockRemoveProvisioner != nil { + return m.MockRemoveProvisioner(ctx, id) + } + return m.MockErr +} diff --git a/api/api_test.go b/api/api_test.go index 5cbce8b3..6a845249 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -3,7 +3,6 @@ package api import ( "bytes" "context" - "crypto" "crypto/dsa" //nolint "crypto/ecdsa" "crypto/ed25519" @@ -32,7 +31,6 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" - "github.com/smallstep/certificates/templates" "go.step.sm/crypto/jose" "golang.org/x/crypto/ssh" ) @@ -551,208 +549,6 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) ( return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err } -type mockAuthority struct { - ret1, ret2 interface{} - err error - authorizeSign func(ott string) ([]provisioner.SignOption, error) - getTLSOptions func() *authority.TLSOptions - root func(shasum string) (*x509.Certificate, error) - sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) - renew func(cert *x509.Certificate) ([]*x509.Certificate, error) - rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) - loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) - loadProvisionerByName func(name string) (provisioner.Interface, error) - getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) - revoke func(context.Context, *authority.RevokeOptions) error - getEncryptedKey func(kid string) (string, error) - getRoots func() ([]*x509.Certificate, error) - getFederation func() ([]*x509.Certificate, error) - signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) - signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) - renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) - rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) - getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) - getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error) - getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error) - getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) - checkSSHHost func(ctx context.Context, principal, token string) (bool, error) - getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error) - version func() authority.Version -} - -// TODO: remove once Authorize is deprecated. -func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { - return m.AuthorizeSign(ott) -} - -func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { - if m.authorizeSign != nil { - return m.authorizeSign(ott) - } - return m.ret1.([]provisioner.SignOption), m.err -} - -func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions { - if m.getTLSOptions != nil { - return m.getTLSOptions() - } - return m.ret1.(*authority.TLSOptions) -} - -func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) { - if m.root != nil { - return m.root(shasum) - } - return m.ret1.(*x509.Certificate), m.err -} - -func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { - if m.sign != nil { - return m.sign(cr, opts, signOpts...) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { - if m.renew != nil { - return m.renew(cert) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { - if m.rekey != nil { - return m.rekey(oldcert, pk) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { - if m.getProvisioners != nil { - return m.getProvisioners(nextCursor, limit) - } - return m.ret1.(provisioner.List), m.ret2.(string), m.err -} - -func (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) { - if m.loadProvisionerByCertificate != nil { - return m.loadProvisionerByCertificate(cert) - } - return m.ret1.(provisioner.Interface), m.err -} - -func (m *mockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { - if m.loadProvisionerByName != nil { - return m.loadProvisionerByName(name) - } - return m.ret1.(provisioner.Interface), m.err -} - -func (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { - if m.revoke != nil { - return m.revoke(ctx, opts) - } - return m.err -} - -func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) { - if m.getEncryptedKey != nil { - return m.getEncryptedKey(kid) - } - return m.ret1.(string), m.err -} - -func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) { - if m.getRoots != nil { - return m.getRoots() - } - return m.ret1.([]*x509.Certificate), m.err -} - -func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) { - if m.getFederation != nil { - return m.getFederation() - } - return m.ret1.([]*x509.Certificate), m.err -} - -func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - if m.signSSH != nil { - return m.signSSH(ctx, key, opts, signOpts...) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) { - if m.signSSHAddUser != nil { - return m.signSSHAddUser(ctx, key, cert) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) { - if m.renewSSH != nil { - return m.renewSSH(ctx, cert) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - if m.rekeySSH != nil { - return m.rekeySSH(ctx, cert, key, signOpts...) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) { - if m.getSSHHosts != nil { - return m.getSSHHosts(ctx, cert) - } - return m.ret1.([]authority.Host), m.err -} - -func (m *mockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) { - if m.getSSHRoots != nil { - return m.getSSHRoots(ctx) - } - return m.ret1.(*authority.SSHKeys), m.err -} - -func (m *mockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) { - if m.getSSHFederation != nil { - return m.getSSHFederation(ctx) - } - return m.ret1.(*authority.SSHKeys), m.err -} - -func (m *mockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { - if m.getSSHConfig != nil { - return m.getSSHConfig(ctx, typ, data) - } - return m.ret1.([]templates.Output), m.err -} - -func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { - if m.checkSSHHost != nil { - return m.checkSSHHost(ctx, principal, token) - } - return m.ret1.(bool), m.err -} - -func (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) { - if m.getSSHBastion != nil { - return m.getSSHBastion(ctx, user, hostname) - } - return m.ret1.(*authority.Bastion), m.err -} - -func (m *mockAuthority) Version() authority.Version { - if m.version != nil { - return m.version() - } - return m.ret1.(authority.Version) -} - func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority @@ -765,7 +561,7 @@ func Test_caHandler_Route(t *testing.T) { fields fields args args }{ - {"ok", fields{&mockAuthority{}}, args{chi.NewRouter()}}, + {"ok", fields{&MockAuthority{}}, args{chi.NewRouter()}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -780,7 +576,7 @@ func Test_caHandler_Route(t *testing.T) { func Test_caHandler_Health(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com/health", nil) w := httptest.NewRecorder() - h := New(&mockAuthority{}).(*caHandler) + h := New(&MockAuthority{}).(*caHandler) h.Health(w, req) res := w.Result() @@ -820,7 +616,7 @@ func Test_caHandler_Root(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ret1: tt.root, err: tt.err}).(*caHandler) + h := New(&MockAuthority{ret1: tt.root, err: tt.err}).(*caHandler) w := httptest.NewRecorder() h.Root(w, req) res := w.Result() @@ -884,7 +680,7 @@ func Test_caHandler_Sign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.signErr, authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return tt.certAttrOpts, tt.autherr @@ -938,7 +734,7 @@ func Test_caHandler_Renew(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.err, getTLSOptions: func() *authority.TLSOptions { return nil @@ -999,7 +795,7 @@ func Test_caHandler_Rekey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.err, getTLSOptions: func() *authority.TLSOptions { return nil @@ -1077,9 +873,9 @@ func Test_caHandler_Provisioners(t *testing.T) { args args statusCode int }{ - {"ok", fields{&mockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), req}, 200}, - {"fail", fields{&mockAuthority{ret1: p, ret2: "", err: fmt.Errorf("the error")}}, args{httptest.NewRecorder(), req}, 500}, - {"limit fail", fields{&mockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), reqLimitFail}, 400}, + {"ok", fields{&MockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), req}, 200}, + {"fail", fields{&MockAuthority{ret1: p, ret2: "", err: fmt.Errorf("the error")}}, args{httptest.NewRecorder(), req}, 500}, + {"limit fail", fields{&MockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), reqLimitFail}, 400}, } expected, err := json.Marshal(pr) @@ -1154,8 +950,8 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { args args statusCode int }{ - {"ok", fields{&mockAuthority{ret1: privKey}}, args{httptest.NewRecorder(), req}, 200}, - {"fail", fields{&mockAuthority{ret1: "", err: fmt.Errorf("not found")}}, args{httptest.NewRecorder(), req}, 404}, + {"ok", fields{&MockAuthority{ret1: privKey}}, args{httptest.NewRecorder(), req}, 200}, + {"fail", fields{&MockAuthority{ret1: "", err: fmt.Errorf("not found")}}, args{httptest.NewRecorder(), req}, 404}, } expected := []byte(`{"key":"` + privKey + `"}`) @@ -1214,7 +1010,7 @@ func Test_caHandler_Roots(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) + h := New(&MockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) req := httptest.NewRequest("GET", "http://example.com/roots", nil) req.TLS = tt.tls w := httptest.NewRecorder() @@ -1260,7 +1056,7 @@ func Test_caHandler_Federation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) + h := New(&MockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) req := httptest.NewRequest("GET", "http://example.com/federation", nil) req.TLS = tt.tls w := httptest.NewRecorder() diff --git a/api/revoke_test.go b/api/revoke_test.go index 4ed4e3fe..b0eaef3d 100644 --- a/api/revoke_test.go +++ b/api/revoke_test.go @@ -106,7 +106,7 @@ func Test_caHandler_Revoke(t *testing.T) { return test{ input: string(input), statusCode: http.StatusOK, - auth: &mockAuthority{ + auth: &MockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, @@ -150,7 +150,7 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusOK, tls: cs, - auth: &mockAuthority{ + auth: &MockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, @@ -185,7 +185,7 @@ func Test_caHandler_Revoke(t *testing.T) { return test{ input: string(input), statusCode: http.StatusInternalServerError, - auth: &mockAuthority{ + auth: &MockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, @@ -207,7 +207,7 @@ func Test_caHandler_Revoke(t *testing.T) { return test{ input: string(input), statusCode: http.StatusForbidden, - auth: &mockAuthority{ + auth: &MockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, diff --git a/api/ssh_test.go b/api/ssh_test.go index a3d7da0d..df9e2f45 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -314,7 +314,7 @@ func Test_caHandler_SSHSign(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return []provisioner.SignOption{}, tt.authErr }, @@ -377,7 +377,7 @@ func Test_caHandler_SSHRoots(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ getSSHRoots: func(ctx context.Context) (*authority.SSHKeys, error) { return tt.keys, tt.keysErr }, @@ -431,7 +431,7 @@ func Test_caHandler_SSHFederation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ getSSHFederation: func(ctx context.Context) (*authority.SSHKeys, error) { return tt.keys, tt.keysErr }, @@ -491,7 +491,7 @@ func Test_caHandler_SSHConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ getSSHConfig: func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { return tt.output, tt.err }, @@ -538,7 +538,7 @@ func Test_caHandler_SSHCheckHost(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ checkSSHHost: func(ctx context.Context, principal, token string) (bool, error) { return tt.exists, tt.err }, @@ -589,7 +589,7 @@ func Test_caHandler_SSHGetHosts(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ getSSHHosts: func(context.Context, *x509.Certificate) ([]authority.Host, error) { return tt.hosts, tt.err }, @@ -644,7 +644,7 @@ func Test_caHandler_SSHBastion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&mockAuthority{ + h := New(&MockAuthority{ getSSHBastion: func(ctx context.Context, user, hostname string) (*authority.Bastion, error) { return tt.bastion, tt.bastionErr }, diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 8cba39c4..88e76a09 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -49,7 +49,7 @@ func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { // provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME // provisioner is set to true and thus has EAB enabled. -func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, error) { +func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *admin.Error) { var ( p provisioner.Interface err error diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go new file mode 100644 index 00000000..84e8e9f5 --- /dev/null +++ b/authority/admin/api/acme_test.go @@ -0,0 +1,416 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" +) + +func TestHandler_requireEABEnabled(t *testing.T) { + type test struct { + ctx context.Context + db admin.DB + auth api.LinkedAuthority + next nextHTTP + err *admin.Error + statusCode int + } + 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") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + err := admin.NewErrorISE("error loading provisioner provName: force") + err.Message = "error loading provisioner provName: force" + return test{ + ctx: ctx, + auth: auth, + err: err, + statusCode: 500, + } + }, + "ok/eab-disabled": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: false, + }, + }, + }, + }, nil + }, + } + err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName") + err.Message = "ACME EAB not enabled for provisioner provName" + return test{ + ctx: ctx, + auth: auth, + db: db, + err: err, + statusCode: 400, + } + }, + "ok/eab-enabled": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: true, + }, + }, + }, + }, nil + }, + } + return test{ + ctx: ctx, + auth: auth, + db: db, + next: func(w http.ResponseWriter, r *http.Request) { + w.Write(nil) // mock response with status 200 + }, + statusCode: 200, + } + }, + } + + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + db: tc.db, + auth: tc.auth, + acmeDB: nil, + } + + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.requireEABEnabled(tc.next)(w, req) + res := w.Result() + + assert.Equals(t, res.StatusCode, tc.statusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + err := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err)) + + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Message, err.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + // nothing to test when the requireEABEnabled middleware succeeds, currently + }) + } +} + +func TestHandler_provisionerHasEABEnabled(t *testing.T) { + type test struct { + db admin.DB + auth api.LinkedAuthority + provisionerName string + want bool + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + return test{ + auth: auth, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "fail/db.GetProvisioner": func(t *testing.T) test { + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + auth: auth, + db: db, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "fail/prov.GetDetails": func(t *testing.T) test { + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: nil, + }, nil + }, + } + return test{ + auth: auth, + db: db, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "fail/details.GetACME": func(t *testing.T) test { + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: nil, + }, + }, + }, nil + }, + } + return test{ + auth: auth, + db: db, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "ok/eab-disabled": func(t *testing.T) test { + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "eab-disabled", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "eab-disabled", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: false, + }, + }, + }, + }, nil + }, + } + return test{ + db: db, + auth: auth, + provisionerName: "eab-disabled", + want: false, + } + }, + "ok/eab-enabled": func(t *testing.T) test { + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "eab-enabled", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "eab-enabled", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: true, + }, + }, + }, + }, nil + }, + } + return test{ + db: db, + auth: auth, + provisionerName: "eab-enabled", + want: true, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + db: tc.db, + auth: tc.auth, + acmeDB: nil, + } + got, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName) + if (err != nil) != (tc.err != nil) { + t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err) + return + } + if tc.err != nil { + // TODO(hs): the output of the diff seems to be equal to each other; not sure why it's marked as different =/ + // opts := []cmp.Option{cmpopts.EquateErrors()} + // if !cmp.Equal(tc.err, err, opts...) { + // t.Errorf("Handler.provisionerHasEABEnabled() diff =\n%v", cmp.Diff(tc.err, err, opts...)) + // } + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Status, err.Status) + assert.Equals(t, tc.err.StatusCode(), err.StatusCode()) + assert.Equals(t, tc.err.Message, err.Message) + assert.Equals(t, tc.err.Detail, err.Detail) + return + } + if got != tc.want { + t.Errorf("Handler.provisionerHasEABEnabled() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { + type fields struct { + Reference string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "ok/empty-reference", + fields: fields{ + Reference: "", + }, + wantErr: false, + }, + { + name: "ok", + fields: fields{ + Reference: "my-eab-reference", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &CreateExternalAccountKeyRequest{ + Reference: tt.fields.Reference, + } + if err := r.Validate(); (err != nil) != tt.wantErr { + t.Errorf("CreateExternalAccountKeyRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index ba13407d..b3ed04bf 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -3,19 +3,18 @@ package api import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" - "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/admin" ) -// Handler is the ACME API request handler. +// Handler is the Admin API request handler. type Handler struct { db admin.DB - auth *authority.Authority + auth api.LinkedAuthority // was: *authority.Authority acmeDB acme.DB } // NewHandler returns a new Authority Config Handler. -func NewHandler(auth *authority.Authority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler { +func NewHandler(auth api.LinkedAuthority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler { return &Handler{ db: adminDB, auth: auth, diff --git a/go.mod b/go.mod index b2014bf4..edf70903 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-kit/kit v0.10.0 // indirect github.com/go-piv/piv-go v1.7.0 github.com/golang/mock v1.6.0 + github.com/google/go-cmp v0.5.6 github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.0.5 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect From 63371a8fb6f8f0ec26bbb4a0e7d24f3b5ddc22d8 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 9 Dec 2021 09:36:03 +0100 Subject: [PATCH 34/47] Add additional tests for ACME EAB Admin --- acme/db.go | 12 +- authority/admin/api/acme.go | 27 +- authority/admin/api/acme_test.go | 750 +++++++++++++++++++++++++++++++ 3 files changed, 775 insertions(+), 14 deletions(-) diff --git a/acme/db.go b/acme/db.go index 73029231..736df5d4 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,7 +19,7 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error - CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) + CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) @@ -54,7 +54,7 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error - MockCreateExternalAccountKey func(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) + MockCreateExternalAccountKey func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) MockGetExternalAccountKeys func(ctx context.Context, provisionerName string, cursor string, limit int) ([]*ExternalAccountKey, string, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) @@ -125,9 +125,9 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error { } // CreateExternalAccountKey mock -func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) { +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { if m.MockCreateExternalAccountKey != nil { - return m.MockCreateExternalAccountKey(ctx, provisionerName, name) + return m.MockCreateExternalAccountKey(ctx, provisionerName, reference) } else if m.MockError != nil { return nil, m.MockError } @@ -156,8 +156,8 @@ func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName, cu // GetExternalAccountKeyByReference mock func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { - if m.MockGetExternalAccountKeys != nil { - return m.GetExternalAccountKeyByReference(ctx, provisionerName, reference) + if m.MockGetExternalAccountKeyByReference != nil { + return m.MockGetExternalAccountKeyByReference(ctx, provisionerName, reference) } else if m.MockError != nil { return nil, m.MockError } diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 88e76a09..39aeed49 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -2,6 +2,8 @@ package api import ( "context" + "errors" + "fmt" "net/http" "github.com/go-chi/chi" @@ -20,6 +22,9 @@ type CreateExternalAccountKeyRequest struct { // Validate validates a new ACME EAB Key request body. func (r *CreateExternalAccountKeyRequest) Validate() error { + if len(r.Reference) > 256 { // an arbitrary, but sensible (IMO), limit + return fmt.Errorf("reference length %d exceeds the maximum (256)", len(r.Reference)) + } return nil } @@ -85,7 +90,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques } if err := body.Validate(); err != nil { - api.WriteError(w, err) + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating request body")) return } @@ -97,9 +102,9 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) // retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for, // but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound. - shouldWriteError := err != nil && acme.ErrNotFound != err + shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound) if shouldWriteError { - api.WriteError(w, err) + api.WriteError(w, admin.WrapErrorISE(err, "could not lookup external account key by reference")) return } // if a key was found, return HTTP 409 conflict @@ -114,7 +119,11 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), prov, reference) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error creating ACME EAB key for provisioner %s and reference %s", prov, reference)) + msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov) + if reference != "" { + msg += fmt.Sprintf(" and reference '%s'", reference) + } + api.WriteError(w, admin.WrapErrorISE(err, msg)) return } @@ -134,7 +143,7 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques keyID := chi.URLParam(r, "id") if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), prov, keyID); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", keyID)) + api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key '%s'", keyID)) return } @@ -165,14 +174,16 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) if reference != "" { key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error getting external account key with reference %s", reference)) + api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference)) return } - keys = []*acme.ExternalAccountKey{key} + if key != nil { + keys = []*acme.ExternalAccountKey{key} + } } else { keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) + api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) return } } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 84e8e9f5..39238852 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -8,16 +8,33 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "testing" + "time" "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) +func readProtoJSON(r io.ReadCloser, m proto.Message) error { + defer r.Close() + data, err := io.ReadAll(r) + if err != nil { + return err + } + return protojson.Unmarshal(data, m) +} + func TestHandler_requireEABEnabled(t *testing.T) { type test struct { ctx context.Context @@ -414,3 +431,736 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { }) } } + +func TestHandler_CreateExternalAccountKey(t *testing.T) { + type test struct { + ctx context.Context + db acme.DB + body []byte + statusCode int + eak *linkedca.EABKey + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + body := []byte("{!?}") + return test{ + ctx: ctx, + body: body, + statusCode: 400, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string", + }, + } + }, + "fail/validate": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: strings.Repeat("A", 257), + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: ctx, + body: body, + statusCode: 400, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error validating request body: reference length 257 exceeds the maximum (256)", + }, + } + }, + "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 500, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "could not lookup external account key by reference: force", + }, + } + }, + "fail/reference-conflict-409": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + past := time.Now().Add(-24 * time.Hour) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: past, + }, nil + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 409, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 409, + Detail: "bad request", + Message: "an ACME EAB key for provisioner provName with reference an-external-key-reference already exists", + }, + } + }, + "fail/acmeDB.CreateExternalAccountKey-no-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error creating ACME EAB key for provisioner 'provName': force", + }, + } + }, + "fail/acmeDB.CreateExternalAccountKey-with-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict + }, + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error creating ACME EAB key for provisioner 'provName' and reference 'an-external-key-reference': force", + }, + } + }, + "ok/no-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + now := time.Now() + db := &acme.MockDB{ + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", reference) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, nil + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 201, + eak: &linkedca.EABKey{ + Id: "eakID", + Provisioner: "provName", + Reference: "", + HmacKey: []byte{1, 3, 3, 7}, + }, + } + }, + "ok/with-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + now := time.Now() + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict + }, + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, nil + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 201, + eak: &linkedca.EABKey{ + Id: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + HmacKey: []byte{1, 3, 3, 7}, + }, + } + }, + } + + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + acmeDB: tc.db, + } + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.CreateExternalAccountKey(w, req) + res := w.Result() + assert.Equals(t, res.StatusCode, tc.statusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } else { + eabKey := &linkedca.EABKey{} + err := readProtoJSON(res.Body, eabKey) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{})} + if !cmp.Equal(tc.eak, eabKey, opts...) { + t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.eak, eabKey, opts...)) + } + } + }) + } +} + +func TestHandler_DeleteExternalAccountKey(t *testing.T) { + type test struct { + ctx context.Context + db acme.DB + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("id", "keyID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "keyID", keyID) + return errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error deleting ACME EAB Key 'keyID': force", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("id", "keyID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "keyID", keyID) + return nil + }, + } + return test{ + ctx: ctx, + db: db, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + acmeDB: tc.db, + } + req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.DeleteExternalAccountKey(w, req) + res := w.Result() + assert.Equals(t, res.StatusCode, tc.statusCode) + + if res.StatusCode >= 400 { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } else { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } + }) + } +} + +func TestHandler_GetExternalAccountKeys(t *testing.T) { + type test struct { + ctx context.Context + db acme.DB + statusCode int + req *http.Request + resp GetExternalAccountKeysResponse + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/parse-cursor": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo?limit=A", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + 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") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + statusCode: 500, + req: req, + db: db, + err: &admin.Error{ + Status: 500, + Type: admin.ErrorServerInternalType.String(), + Detail: "the server experienced an internal error", + Message: "error retrieving external account key with reference 'an-external-key-reference': force", + }, + } + }, + "fail/acmeDB.GetExternalAccountKeys": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") + }, + } + return test{ + ctx: ctx, + statusCode: 500, + req: req, + db: db, + err: &admin.Error{ + Status: 500, + Type: admin.ErrorServerInternalType.String(), + Detail: "the server experienced an internal error", + Message: "error retrieving external account keys: force", + }, + } + }, + "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") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, nil // returning nil; no key found + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{}, + NextCursor: "", + }, + db: db, + err: nil, + } + }, + "ok/reference-found": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("ref", "an-external-key-reference") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now().Add(-24 * time.Hour) + var boundAt time.Time + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + CreatedAt: createdAt, + }, nil + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{ + { + Id: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + }, + }, + NextCursor: "", + }, + db: db, + err: nil, + } + }, + "ok/multiple-keys": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now().Add(-24 * time.Hour) + var boundAt time.Time + boundAtSet := time.Now().Add(-12 * time.Hour) + db := &acme.MockDB{ + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return []*acme.ExternalAccountKey{ + { + ID: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, + { + ID: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt.Add(1 * time.Hour), + }, + { + ID: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + BoundAt: boundAtSet, + AccountID: "accountID", + }, + }, "nextCursorValue", nil + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{ + { + Id: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + CreatedAt: timestamppb.New(createdAt.Add(1 * time.Hour)), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAtSet), + Account: "accountID", + }, + }, + NextCursor: "nextCursorValue", + }, + db: db, + err: nil, + } + }, + "ok/multiple-keys-with-cursor-and-limit": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo?cursor=eakID1&limit=10", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now().Add(-24 * time.Hour) + var boundAt time.Time + boundAtSet := time.Now().Add(-12 * time.Hour) + db := &acme.MockDB{ + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "eakID1", cursor) + assert.Equals(t, 10, limit) + return []*acme.ExternalAccountKey{ + { + ID: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, + { + ID: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt.Add(1 * time.Hour), + }, + { + ID: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + BoundAt: boundAtSet, + AccountID: "accountID", + }, + }, "eakID4", nil + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{ + { + Id: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + CreatedAt: timestamppb.New(createdAt.Add(1 * time.Hour)), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAtSet), + Account: "accountID", + }, + }, + NextCursor: "eakID4", + }, + db: db, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + acmeDB: tc.db, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetExternalAccountKeys(w, req) + res := w.Result() + assert.Equals(t, res.StatusCode, tc.statusCode) + + if res.StatusCode >= 400 { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } else { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := GetExternalAccountKeysResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + } + } + }) + } +} From bd169f505f8a7a995b019edd541cc80320438807 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 9 Dec 2021 15:24:14 +0100 Subject: [PATCH 35/47] Add Admin API Middleware tests --- authority/admin/api/acme_test.go | 19 ++- authority/admin/api/middleware_test.go | 226 +++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 authority/admin/api/middleware_test.go diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 39238852..2c09b06f 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -161,7 +161,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { h.requireEABEnabled(tc.next)(w, req) res := w.Result() - assert.Equals(t, res.StatusCode, tc.statusCode) + assert.Equals(t, tc.statusCode, res.StatusCode) body, err := io.ReadAll(res.Body) res.Body.Close() @@ -178,8 +178,6 @@ func TestHandler_requireEABEnabled(t *testing.T) { assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) return } - - // nothing to test when the requireEABEnabled middleware succeeds, currently }) } } @@ -405,6 +403,13 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { fields fields wantErr bool }{ + { + name: "fail/reference-too-long", + fields: fields{ + Reference: strings.Repeat("A", 257), + }, + wantErr: true, + }, { name: "ok/empty-reference", fields: fields{ @@ -702,7 +707,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { w := httptest.NewRecorder() h.CreateExternalAccountKey(w, req) res := w.Result() - assert.Equals(t, res.StatusCode, tc.statusCode) + assert.Equals(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { @@ -797,7 +802,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { w := httptest.NewRecorder() h.DeleteExternalAccountKey(w, req) res := w.Result() - assert.Equals(t, res.StatusCode, tc.statusCode) + assert.Equals(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) @@ -1131,7 +1136,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { w := httptest.NewRecorder() h.GetExternalAccountKeys(w, req) res := w.Result() - assert.Equals(t, res.StatusCode, tc.statusCode) + assert.Equals(t, tc.statusCode, res.StatusCode) if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) @@ -1158,7 +1163,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{}, timestamppb.Timestamp{})} if !cmp.Equal(tc.resp, response, opts...) { - t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + t.Errorf("h.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...)) } } }) diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go new file mode 100644 index 00000000..3231cc6d --- /dev/null +++ b/authority/admin/api/middleware_test.go @@ -0,0 +1,226 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" + "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestHandler_requireAPIEnabled(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + next nextHTTP + err *admin.Error + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.IsAdminAPIEnabled": func(t *testing.T) test { + return test{ + ctx: context.Background(), + auth: &api.MockAuthority{ + MockIsAdminAPIEnabled: func() bool { + return false + }, + }, + err: &admin.Error{ + Type: admin.ErrorNotImplementedType.String(), + Status: 501, + Detail: "not implemented", + Message: "administration API not enabled", + }, + statusCode: 501, + } + }, + "ok": func(t *testing.T) test { + auth := &api.MockAuthority{ + MockIsAdminAPIEnabled: func() bool { + return true + }, + } + next := func(w http.ResponseWriter, r *http.Request) { + w.Write(nil) // mock response with status 200 + } + return test{ + ctx: context.Background(), + auth: auth, + next: next, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.requireAPIEnabled(tc.next)(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + err := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err)) + + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Message, err.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + // nothing to test when the requireAPIEnabled middleware succeeds, currently + + }) + } +} + +func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + req *http.Request + next nextHTTP + err *admin.Error + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/missing-authorization-token": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + req.Header["Authorization"] = []string{""} + return test{ + ctx: context.Background(), + req: req, + statusCode: 401, + err: &admin.Error{ + Type: admin.ErrorUnauthorizedType.String(), + Status: 401, + Detail: "unauthorized", + Message: "missing authorization header token", + }, + } + }, + "fail/auth.AuthorizeAdminToken": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + req.Header["Authorization"] = []string{"token"} + auth := &api.MockAuthority{ + MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { + assert.Equals(t, "token", token) + return nil, admin.NewError( + admin.ErrorUnauthorizedType, + "not authorized", + ) + }, + } + return test{ + ctx: context.Background(), + auth: auth, + req: req, + statusCode: 401, + err: &admin.Error{ + Type: admin.ErrorUnauthorizedType.String(), + Status: 401, + Detail: "unauthorized", + Message: "not authorized", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + req.Header["Authorization"] = []string{"token"} + createdAt := time.Now() + var deletedAt time.Time + admin := &linkedca.Admin{ + Id: "adminID", + AuthorityId: "authorityID", + Subject: "admin", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &api.MockAuthority{ + MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { + assert.Equals(t, "token", token) + return admin, nil + }, + } + next := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + a := ctx.Value(adminContextKey) // verifying that the context now has a linkedca.Admin + adm, ok := a.(*linkedca.Admin) + if !ok { + t.Errorf("expected *linkedca.Admin; got %T", a) + return + } + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(admin, adm, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(admin, adm, opts...)) + } + w.Write(nil) // mock response with status 200 + } + return test{ + ctx: context.Background(), + auth: auth, + req: req, + next: next, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.extractAuthorizeTokenAdmin(tc.next)(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + err := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err)) + + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Message, err.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + }) + } +} From 43a78f495f9e3901f60ec69d2124d0680eac416d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 9 Dec 2021 17:29:23 +0100 Subject: [PATCH 36/47] Add tests for Admin API --- authority/admin/api/acme_test.go | 39 +- authority/admin/api/admin_test.go | 811 ++++++++++++++++++++++++++++++ 2 files changed, 832 insertions(+), 18 deletions(-) create mode 100644 authority/admin/api/admin_test.go diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 2c09b06f..114629fc 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -723,18 +723,19 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, adminErr.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) - } else { - eabKey := &linkedca.EABKey{} - err := readProtoJSON(res.Body, eabKey) - assert.FatalError(t, err) + return + } - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + eabKey := &linkedca.EABKey{} + err := readProtoJSON(res.Body, eabKey) + assert.FatalError(t, err) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) - opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{})} - if !cmp.Equal(tc.eak, eabKey, opts...) { - t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.eak, eabKey, opts...)) - } + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{})} + if !cmp.Equal(tc.eak, eabKey, opts...) { + t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.eak, eabKey, opts...)) } + }) } } @@ -817,16 +818,18 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, adminErr.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) - } else { - body, err := io.ReadAll(res.Body) - res.Body.Close() - assert.FatalError(t, err) - - response := DeleteResponse{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) - assert.Equals(t, "ok", response.Status) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + }) } } diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go new file mode 100644 index 00000000..da044d58 --- /dev/null +++ b/authority/admin/api/admin_test.go @@ -0,0 +1,811 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestCreateAdminRequest_Validate(t *testing.T) { + type fields struct { + Subject string + Provisioner string + Type linkedca.Admin_Type + } + tests := []struct { + name string + fields fields + err *admin.Error + }{ + { + name: "fail/subject-empty", + fields: fields{ + Subject: "", + Provisioner: "", + Type: 0, + }, + err: admin.NewError(admin.ErrorBadRequestType, "subject cannot be empty"), + }, + { + name: "fail/provisioner-empty", + fields: fields{ + Subject: "admin", + Provisioner: "", + Type: 0, + }, + err: admin.NewError(admin.ErrorBadRequestType, "provisioner cannot be empty"), + }, + { + name: "fail/invalid-type", + fields: fields{ + Subject: "admin", + Provisioner: "prov", + Type: -1, + }, + err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"), + }, + { + name: "ok", + fields: fields{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + }, + err: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + car := &CreateAdminRequest{ + Subject: tt.fields.Subject, + Provisioner: tt.fields.Provisioner, + Type: tt.fields.Type, + } + err := car.Validate() + + if (err != nil) != (tt.err != nil) { + t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil)) + return + } + + if err != nil { + assert.Type(t, &admin.Error{}, err) + adminErr, _ := err.(*admin.Error) + assert.Equals(t, tt.err.Type, adminErr.Type) + assert.Equals(t, tt.err.Detail, adminErr.Detail) + assert.Equals(t, tt.err.Status, adminErr.Status) + assert.Equals(t, tt.err.Message, adminErr.Message) + } + }) + } +} + +func TestUpdateAdminRequest_Validate(t *testing.T) { + type fields struct { + Type linkedca.Admin_Type + } + tests := []struct { + name string + fields fields + err *admin.Error + }{ + { + name: "fail/invalid-type", + fields: fields{ + Type: -1, + }, + err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"), + }, + { + name: "ok", + fields: fields{ + Type: linkedca.Admin_SUPER_ADMIN, + }, + err: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uar := &UpdateAdminRequest{ + Type: tt.fields.Type, + } + + err := uar.Validate() + + if (err != nil) != (tt.err != nil) { + t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil)) + return + } + + if err != nil { + assert.Type(t, &admin.Error{}, err) + adminErr, _ := err.(*admin.Error) + assert.Equals(t, tt.err.Type, adminErr.Type) + assert.Equals(t, tt.err.Detail, adminErr.Detail) + assert.Equals(t, tt.err.Status, adminErr.Status) + assert.Equals(t, tt.err.Message, adminErr.Message) + } + }) + } +} + +func TestHandler_GetAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadAdminByID-not-found": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { + assert.Equals(t, "adminID", id) + return nil, false + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 404, + err: &admin.Error{ + Type: admin.ErrorNotFoundType.String(), + Status: 404, + Detail: "resource not found", + Message: "admin adminID not found", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + adm := &linkedca.Admin{ + Id: "adminID", + AuthorityId: "authorityID", + Subject: "admin", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &api.MockAuthority{ + MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { + assert.Equals(t, "adminID", id) + return adm, true + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 200, + err: nil, + adm: adm, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetAdmin(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} + +func TestHandler_GetAdmins(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + req *http.Request + statusCode int + err *admin.Error + resp GetAdminsResponse + } + var tests = map[string]func(t *testing.T) test{ + "fail/parse-cursor": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo?limit=A", nil) + return test{ + ctx: context.Background(), + req: req, + statusCode: 400, + 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/auth.GetAdmins": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + auth := &api.MockAuthority{ + MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Status: 500, + Type: admin.ErrorServerInternalType.String(), + Detail: "the server experienced an internal error", + Message: "error retrieving paginated admins: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + createdAt := time.Now() + var deletedAt time.Time + adm1 := &linkedca.Admin{ + Id: "adminID1", + AuthorityId: "authorityID1", + Subject: "admin1", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + adm2 := &linkedca.Admin{ + Id: "adminID2", + AuthorityId: "authorityID", + Subject: "admin2", + ProvisionerId: "provID", + Type: linkedca.Admin_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &api.MockAuthority{ + MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return []*linkedca.Admin{ + adm1, + adm2, + }, "nextCursorValue", nil + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 200, + err: nil, + resp: GetAdminsResponse{ + Admins: []*linkedca.Admin{ + adm1, + adm2, + }, + NextCursor: "nextCursorValue", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetAdmins(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + response := GetAdminsResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("GetAdmins diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + } + }) + } +} + +func TestHandler_CreateAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + body []byte + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string", + }, + } + }, + "fail/validate": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "", + Provisioner: "", + Type: -1, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "subject cannot be empty", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner prov: force", + }, + } + }, + "fail/auth.StoreAdmin": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return &provisioner.ACME{ + ID: "provID", + Name: "prov", + }, nil + }, + MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + assert.Equals(t, "admin", adm.Subject) + assert.Equals(t, "provID", prov.GetID()) + return errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error storing admin: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return &provisioner.ACME{ + ID: "provID", + Name: "prov", + }, nil + }, + MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + assert.Equals(t, "admin", adm.Subject) + assert.Equals(t, "provID", prov.GetID()) + return nil + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 201, + err: nil, + adm: &linkedca.Admin{ + ProvisionerId: "provID", + Subject: "admin", + Type: linkedca.Admin_SUPER_ADMIN, + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.CreateAdmin(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("h.CreateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} + +func TestHandler_DeleteAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.RemoveAdmin": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockRemoveAdmin: func(ctx context.Context, id string) error { + assert.Equals(t, "adminID", id) + return errors.New("force") + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error deleting admin adminID: force", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockRemoveAdmin: func(ctx context.Context, id string) error { + assert.Equals(t, "adminID", id) + return nil + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.DeleteAdmin(w, req) + res := w.Result() + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + }) + } +} + +func TestHandler_UpdateAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + body []byte + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string", + }, + } + }, + "fail/validate": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: -1, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "invalid value for admin type", + }, + } + }, + "fail/auth.UpdateAdmin": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: linkedca.Admin_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + assert.Equals(t, "adminID", id) + assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error updating admin adminID: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: linkedca.Admin_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + adm := &linkedca.Admin{ + Id: "adminID", + ProvisionerId: "provID", + Subject: "admin", + Type: linkedca.Admin_SUPER_ADMIN, + } + auth := &api.MockAuthority{ + MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + assert.Equals(t, "adminID", id) + assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) + return adm, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 200, + err: nil, + adm: adm, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.UpdateAdmin(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("h.UpdateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} From 5f224b729ebd8acef01e02519297961262cfda95 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 9 Dec 2021 23:15:38 +0100 Subject: [PATCH 37/47] Add tests for Provisioner Admin API --- api/api.go | 6 +- authority/admin/api/acme_test.go | 24 +- authority/admin/api/provisioner.go | 2 +- authority/admin/api/provisioner_test.go | 1105 +++++++++++++++++++++++ 4 files changed, 1122 insertions(+), 15 deletions(-) create mode 100644 authority/admin/api/provisioner_test.go diff --git a/api/api.go b/api/api.go index bc61ad3e..9e6ee301 100644 --- a/api/api.go +++ b/api/api.go @@ -487,7 +487,7 @@ type MockAuthority struct { rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) MockLoadProvisionerByName func(name string) (provisioner.Interface, error) - getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) + MockGetProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) revoke func(context.Context, *authority.RevokeOptions) error getEncryptedKey func(kid string) (string, error) getRoots func() ([]*x509.Certificate, error) @@ -567,8 +567,8 @@ func (m *MockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([ } func (m *MockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { - if m.getProvisioners != nil { - return m.getProvisioners(nextCursor, limit) + if m.MockGetProvisioners != nil { + return m.MockGetProvisioners(nextCursor, limit) } return m.ret1.(provisioner.List), m.ret2.(string), m.err } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 114629fc..15c581f4 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -1154,21 +1154,23 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { assert.Equals(t, tc.err.StatusCode(), res.StatusCode) assert.Equals(t, tc.err.Detail, adminErr.Detail) assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) - } else { - body, err := io.ReadAll(res.Body) - res.Body.Close() - assert.FatalError(t, err) + return + } - response := GetExternalAccountKeysResponse{} - assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) - assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + response := GetExternalAccountKeysResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) - opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{}, timestamppb.Timestamp{})} - if !cmp.Equal(tc.resp, response, opts...) { - t.Errorf("h.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...)) - } + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("h.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...)) } + }) } } diff --git a/authority/admin/api/provisioner.go b/authority/admin/api/provisioner.go index fd1a02d5..d111f1e6 100644 --- a/authority/admin/api/provisioner.go +++ b/authority/admin/api/provisioner.go @@ -54,7 +54,7 @@ func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) { cursor, limit, err := api.ParseCursor(r) if err != nil { api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, - "error parsing cursor & limit query params")) + "error parsing cursor and limit from query params")) return } diff --git a/authority/admin/api/provisioner_test.go b/authority/admin/api/provisioner_test.go new file mode 100644 index 00000000..68a54fe8 --- /dev/null +++ b/authority/admin/api/provisioner_test.go @@ -0,0 +1,1105 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestHandler_GetProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + db admin.DB + req *http.Request + statusCode int + err *admin.Error + prov *linkedca.Provisioner + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadProvisionerByID": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo?id=provID", nil) + chiCtx := chi.NewRouteContext() + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provID: force", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provName: force", + }, + } + }, + "fail/db.GetProvisioner": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.ACME{ + ID: "acmeID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "acmeID", id) + return nil, admin.NewErrorISE("error loading provisioner provName: force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provName: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.ACME{ + ID: "acmeID", + Name: "provName", + }, nil + }, + } + prov := &linkedca.Provisioner{ + Id: "acmeID", + Type: linkedca.Provisioner_ACME, + Name: "provName", // TODO(hs): other fields too? + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "acmeID", id) + return prov, nil + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + db: db, + statusCode: 200, + err: nil, + prov: prov, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + db: tc.db, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + prov := &linkedca.Provisioner{} + err := readProtoJSON(res.Body, prov) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.prov, prov, opts...) { + t.Errorf("h.GetProvisioner diff =\n%s", cmp.Diff(tc.prov, prov, opts...)) + } + }) + } +} + +func TestHandler_GetProvisioners(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + req *http.Request + statusCode int + err *admin.Error + resp GetProvisionersResponse + } + var tests = map[string]func(t *testing.T) test{ + "fail/parse-cursor": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo?limit=X", nil) + return test{ + ctx: context.Background(), + 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 'X' is not an integer: strconv.Atoi: parsing \"X\": invalid syntax", + }, + } + }, + "fail/auth.GetProvisioners": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + auth := &api.MockAuthority{ + MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: "", + Status: 500, + Detail: "", + Message: "The certificate authority encountered an Internal Server Error. Please see the certificate authority logs for more info.", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + provisioners := provisioner.List{ + &provisioner.OIDC{ + Type: "OIDC", + Name: "oidcProv", + }, + &provisioner.ACME{ + Type: "ACME", + Name: "provName", + ForceCN: false, + RequireEAB: false, + }, + } + auth := &api.MockAuthority{ + MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return provisioners, "nextCursorValue", nil + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 200, + err: nil, + resp: GetProvisionersResponse{ + Provisioners: provisioners, + NextCursor: "nextCursorValue", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetProvisioners(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := GetProvisionersResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(provisioner.ACME{}, provisioner.OIDC{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("h.GetProvisioners diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + } + }) + } +} + +func TestHandler_CreateProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + body []byte + statusCode int + err *admin.Error + prov *linkedca.Provisioner + } + var tests = map[string]func(t *testing.T) test{ + "fail/readProtoJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 500, + err: &admin.Error{ // TODO(hs): this probably needs a better error + Type: "", + Status: 500, + Detail: "", + Message: "", + }, + } + }, + // TODO(hs): ValidateClaims can't be mocked atm + // "fail/authority.ValidateClaims": func(t *testing.T) test { + // return test{} + // }, + "fail/auth.StoreProvisioner": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error { + assert.Equals(t, "provID", prov.Id) + return errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error storing provisioner provName: force", + }, + } + }, + "ok": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error { + assert.Equals(t, "provID", prov.Id) + return nil + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 201, + err: nil, + prov: prov, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.CreateProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + prov := &linkedca.Provisioner{} + err := readProtoJSON(res.Body, prov) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.prov, prov, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.prov, prov, opts...)) + } + }) + } +} + +func TestHandler_DeleteProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + req *http.Request + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadProvisionerByID": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo?id=provID", nil) + chiCtx := chi.NewRouteContext() + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provID: force", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provName: force", + }, + } + }, + "fail/auth.RemoveProvisioner": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + Type: "OIDC", + }, nil + }, + MockRemoveProvisioner: func(ctx context.Context, id string) error { + assert.Equals(t, "provID", id) + return errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error removing provisioner provName: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + Type: "OIDC", + }, nil + }, + MockRemoveProvisioner: func(ctx context.Context, id string) error { + assert.Equals(t, "provID", id) + return nil + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.DeleteProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + }) + } +} + +func TestHandler_UpdateProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + body []byte + db admin.DB + statusCode int + err *admin.Error + prov *linkedca.Provisioner + } + var tests = map[string]func(t *testing.T) test{ + "fail/readProtoJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 500, + err: &admin.Error{ // TODO(hs): this probably needs a better error + Type: "", + Status: 500, + Detail: "", + Message: "", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + // return &provisioner.OIDC{ + // ID: "provID", + // Name: "provName", + // }, nil + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner from cached configuration 'provName': force", + }, + } + }, + "fail/db.GetProvisioner": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner from db 'provID': force", + }, + } + }, + "fail/change-id-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "differentProvID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner ID", + }, + } + }, + "fail/change-type-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_JWK, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner type", + }, + } + }, + "fail/change-authority-id-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "differentAuthorityID", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner authorityID", + }, + } + }, + "fail/change-createdAt-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(time.Now().Add(-1 * time.Hour)), + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner createdAt", + }, + } + }, + "fail/change-deletedAt-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(time.Now()), + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner deletedAt", + }, + } + }, + // TODO(hs): ValidateClaims can't be mocked atm + //"fail/ValidateClaims": func(t *testing.T) test { return test{} }, + "fail/auth.UpdateProvisioner": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + assert.Equals(t, "provID", nu.Id) + assert.Equals(t, "provName", nu.Name) + return errors.New("force") + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: "", // TODO(hs): this error can be improved + Status: 500, + Detail: "", + Message: "", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_OIDC{ + OIDC: &linkedca.OIDCProvisioner{ + ClientId: "new-client-id", + ClientSecret: "new-client-secret", + }, + }, + }, + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + assert.Equals(t, "provID", nu.Id) + assert.Equals(t, "provName", nu.Name) + return nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + db: db, + statusCode: 200, + prov: prov, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + db: tc.db, + } + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.UpdateProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + prov := &linkedca.Provisioner{} + err := readProtoJSON(res.Body, prov) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{ + cmpopts.IgnoreUnexported( + linkedca.Provisioner{}, linkedca.ProvisionerDetails{}, linkedca.ProvisionerDetails_OIDC{}, + linkedca.OIDCProvisioner{}, timestamppb.Timestamp{}, + ), + } + if !cmp.Equal(tc.prov, prov, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.prov, prov, opts...)) + } + }) + } +} From f9ae875f9d09e699d461a70e5cb4fd76d2e1c6a1 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 20 Dec 2021 14:30:01 +0100 Subject: [PATCH 38/47] Use short if-style statements --- acme/api/account.go | 3 +-- acme/db/nosql/account.go | 3 +-- authority/admin/api/acme.go | 12 ++++-------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index a917b0e0..658c40a8 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -297,8 +297,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc } var payloadJWK *jose.JSONWebKey - err = json.Unmarshal(payload, &payloadJWK) - if err != nil { + if err = json.Unmarshal(payload, &payloadJWK); err != nil { return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") } diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 9464e9a4..ca51e8ba 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -251,8 +251,7 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, key return errors.Wrapf(err, "error deleting ACME EAB Key Reference with Key ID %s and reference %s", keyID, dbeak.Reference) } } - err = db.db.Del(externalAccountKeyTable, []byte(keyID)) - if err != nil { + if err = db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) } return nil diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 39aeed49..48e81ecc 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -102,8 +102,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) // retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for, // but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound. - shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound) - if shouldWriteError { + if shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound); shouldWriteError { api.WriteError(w, admin.WrapErrorISE(err, "could not lookup external account key by reference")) return } @@ -164,16 +163,14 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) limit int ) - cursor, limit, err = api.ParseCursor(r) - if err != nil { + 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 } if reference != "" { - key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) - if err != nil { + if key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference)) return } @@ -181,8 +178,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) keys = []*acme.ExternalAccountKey{key} } } else { - keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit) - if err != nil { + if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) return } From 5fe990917434f7a82017969df6b3fccfac784846 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 22 Dec 2021 15:30:40 +0100 Subject: [PATCH 39/47] Refactor AdminAuthority interface --- api/api.go | 311 ------------------------ api/api_test.go | 230 +++++++++++++++++- api/revoke_test.go | 8 +- api/ssh_test.go | 14 +- authority/admin/api/acme.go | 2 +- authority/admin/api/acme_test.go | 40 ++- authority/admin/api/admin.go | 18 ++ authority/admin/api/admin_test.go | 142 +++++++++-- authority/admin/api/handler.go | 4 +- authority/admin/api/middleware_test.go | 13 +- authority/admin/api/provisioner_test.go | 57 ++--- 11 files changed, 424 insertions(+), 415 deletions(-) diff --git a/api/api.go b/api/api.go index 9e6ee301..16e24bb2 100644 --- a/api/api.go +++ b/api/api.go @@ -25,9 +25,6 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" - "github.com/smallstep/certificates/templates" - "go.step.sm/linkedca" - "golang.org/x/crypto/ssh" ) // Authority is the interface implemented by a CA authority. @@ -51,21 +48,6 @@ type Authority interface { Version() authority.Version } -type LinkedAuthority interface { // TODO(hs): name is not great; it is related to LinkedCA, though - Authority - IsAdminAPIEnabled() bool - LoadAdminByID(id string) (*linkedca.Admin, bool) - GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) - StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error - UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) - RemoveAdmin(ctx context.Context, id string) error - AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) - StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error - LoadProvisionerByID(id string) (provisioner.Interface, error) - UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error - RemoveProvisioner(ctx context.Context, id string) error -} - // TimeDuration is an alias of provisioner.TimeDuration type TimeDuration = provisioner.TimeDuration @@ -475,296 +457,3 @@ func fmtPublicKey(cert *x509.Certificate) string { } return fmt.Sprintf("%s %s", cert.PublicKeyAlgorithm, params) } - -type MockAuthority struct { - ret1, ret2 interface{} - err error - authorizeSign func(ott string) ([]provisioner.SignOption, error) - getTLSOptions func() *authority.TLSOptions - root func(shasum string) (*x509.Certificate, error) - sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) - renew func(cert *x509.Certificate) ([]*x509.Certificate, error) - rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) - loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) - MockLoadProvisionerByName func(name string) (provisioner.Interface, error) - MockGetProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) - revoke func(context.Context, *authority.RevokeOptions) error - getEncryptedKey func(kid string) (string, error) - getRoots func() ([]*x509.Certificate, error) - getFederation func() ([]*x509.Certificate, error) - signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) - signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) - renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) - rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) - getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) - getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error) - getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error) - getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) - checkSSHHost func(ctx context.Context, principal, token string) (bool, error) - getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error) - version func() authority.Version - - MockRet1, MockRet2 interface{} // TODO: refactor the ret1/ret2 into those two - MockErr error - MockIsAdminAPIEnabled func() bool - MockLoadAdminByID func(id string) (*linkedca.Admin, bool) - MockGetAdmins func(cursor string, limit int) ([]*linkedca.Admin, string, error) - MockStoreAdmin func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error - MockUpdateAdmin func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) - MockRemoveAdmin func(ctx context.Context, id string) error - MockAuthorizeAdminToken func(r *http.Request, token string) (*linkedca.Admin, error) - MockStoreProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error - MockLoadProvisionerByID func(id string) (provisioner.Interface, error) - MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error - MockRemoveProvisioner func(ctx context.Context, id string) error -} - -// TODO: remove once Authorize is deprecated. -func (m *MockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { - return m.AuthorizeSign(ott) -} - -func (m *MockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { - if m.authorizeSign != nil { - return m.authorizeSign(ott) - } - return m.ret1.([]provisioner.SignOption), m.err -} - -func (m *MockAuthority) GetTLSOptions() *authority.TLSOptions { - if m.getTLSOptions != nil { - return m.getTLSOptions() - } - return m.ret1.(*authority.TLSOptions) -} - -func (m *MockAuthority) Root(shasum string) (*x509.Certificate, error) { - if m.root != nil { - return m.root(shasum) - } - return m.ret1.(*x509.Certificate), m.err -} - -func (m *MockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { - if m.sign != nil { - return m.sign(cr, opts, signOpts...) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *MockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { - if m.renew != nil { - return m.renew(cert) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *MockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { - if m.rekey != nil { - return m.rekey(oldcert, pk) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *MockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { - if m.MockGetProvisioners != nil { - return m.MockGetProvisioners(nextCursor, limit) - } - return m.ret1.(provisioner.List), m.ret2.(string), m.err -} - -func (m *MockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) { - if m.loadProvisionerByCertificate != nil { - return m.loadProvisionerByCertificate(cert) - } - return m.ret1.(provisioner.Interface), m.err -} - -func (m *MockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { - if m.MockLoadProvisionerByName != nil { - return m.MockLoadProvisionerByName(name) - } - return m.ret1.(provisioner.Interface), m.err -} - -func (m *MockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { - if m.revoke != nil { - return m.revoke(ctx, opts) - } - return m.err -} - -func (m *MockAuthority) GetEncryptedKey(kid string) (string, error) { - if m.getEncryptedKey != nil { - return m.getEncryptedKey(kid) - } - return m.ret1.(string), m.err -} - -func (m *MockAuthority) GetRoots() ([]*x509.Certificate, error) { - if m.getRoots != nil { - return m.getRoots() - } - return m.ret1.([]*x509.Certificate), m.err -} - -func (m *MockAuthority) GetFederation() ([]*x509.Certificate, error) { - if m.getFederation != nil { - return m.getFederation() - } - return m.ret1.([]*x509.Certificate), m.err -} - -func (m *MockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - if m.signSSH != nil { - return m.signSSH(ctx, key, opts, signOpts...) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *MockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) { - if m.signSSHAddUser != nil { - return m.signSSHAddUser(ctx, key, cert) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *MockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) { - if m.renewSSH != nil { - return m.renewSSH(ctx, cert) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *MockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - if m.rekeySSH != nil { - return m.rekeySSH(ctx, cert, key, signOpts...) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *MockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) { - if m.getSSHHosts != nil { - return m.getSSHHosts(ctx, cert) - } - return m.ret1.([]authority.Host), m.err -} - -func (m *MockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) { - if m.getSSHRoots != nil { - return m.getSSHRoots(ctx) - } - return m.ret1.(*authority.SSHKeys), m.err -} - -func (m *MockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) { - if m.getSSHFederation != nil { - return m.getSSHFederation(ctx) - } - return m.ret1.(*authority.SSHKeys), m.err -} - -func (m *MockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { - if m.getSSHConfig != nil { - return m.getSSHConfig(ctx, typ, data) - } - return m.ret1.([]templates.Output), m.err -} - -func (m *MockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { - if m.checkSSHHost != nil { - return m.checkSSHHost(ctx, principal, token) - } - return m.ret1.(bool), m.err -} - -func (m *MockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) { - if m.getSSHBastion != nil { - return m.getSSHBastion(ctx, user, hostname) - } - return m.ret1.(*authority.Bastion), m.err -} - -func (m *MockAuthority) Version() authority.Version { - if m.version != nil { - return m.version() - } - return m.ret1.(authority.Version) -} - -func (m *MockAuthority) IsAdminAPIEnabled() bool { - if m.MockIsAdminAPIEnabled != nil { - return m.MockIsAdminAPIEnabled() - } - return m.MockRet1.(bool) -} - -func (m *MockAuthority) LoadAdminByID(id string) (*linkedca.Admin, bool) { - if m.MockLoadAdminByID != nil { - return m.MockLoadAdminByID(id) - } - return m.MockRet1.(*linkedca.Admin), m.MockRet2.(bool) -} - -func (m *MockAuthority) GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) { - if m.MockGetAdmins != nil { - return m.MockGetAdmins(cursor, limit) - } - return m.MockRet1.([]*linkedca.Admin), m.MockRet2.(string), m.MockErr -} - -func (m *MockAuthority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { - if m.MockStoreAdmin != nil { - return m.MockStoreAdmin(ctx, adm, prov) - } - return m.MockErr -} - -func (m *MockAuthority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { - if m.MockUpdateAdmin != nil { - return m.MockUpdateAdmin(ctx, id, nu) - } - return m.MockRet1.(*linkedca.Admin), m.MockErr -} - -func (m *MockAuthority) RemoveAdmin(ctx context.Context, id string) error { - if m.MockRemoveAdmin != nil { - return m.MockRemoveAdmin(ctx, id) - } - return m.MockErr -} - -func (m *MockAuthority) AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) { - if m.MockAuthorizeAdminToken != nil { - return m.MockAuthorizeAdminToken(r, token) - } - return m.MockRet1.(*linkedca.Admin), m.MockErr -} - -func (m *MockAuthority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { - if m.MockStoreProvisioner != nil { - return m.MockStoreProvisioner(ctx, prov) - } - return m.MockErr -} - -func (m *MockAuthority) LoadProvisionerByID(id string) (provisioner.Interface, error) { - if m.MockLoadProvisionerByID != nil { - return m.MockLoadProvisionerByID(id) - } - return m.MockRet1.(provisioner.Interface), m.MockErr -} - -func (m *MockAuthority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error { - if m.MockUpdateProvisioner != nil { - return m.MockUpdateProvisioner(ctx, nu) - } - return m.MockErr -} - -func (m *MockAuthority) RemoveProvisioner(ctx context.Context, id string) error { - if m.MockRemoveProvisioner != nil { - return m.MockRemoveProvisioner(ctx, id) - } - return m.MockErr -} diff --git a/api/api_test.go b/api/api_test.go index 6a845249..c7528f9b 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -3,6 +3,7 @@ package api import ( "bytes" "context" + "crypto" "crypto/dsa" //nolint "crypto/ecdsa" "crypto/ed25519" @@ -31,6 +32,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" + "github.com/smallstep/certificates/templates" "go.step.sm/crypto/jose" "golang.org/x/crypto/ssh" ) @@ -165,6 +167,208 @@ func parseCertificateRequest(data string) *x509.CertificateRequest { return csr } +type mockAuthority struct { + ret1, ret2 interface{} + err error + authorizeSign func(ott string) ([]provisioner.SignOption, error) + getTLSOptions func() *authority.TLSOptions + root func(shasum string) (*x509.Certificate, error) + sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) + renew func(cert *x509.Certificate) ([]*x509.Certificate, error) + rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) + loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) + loadProvisionerByName func(name string) (provisioner.Interface, error) + getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) + revoke func(context.Context, *authority.RevokeOptions) error + getEncryptedKey func(kid string) (string, error) + getRoots func() ([]*x509.Certificate, error) + getFederation func() ([]*x509.Certificate, error) + signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) + renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) + rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) + getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error) + getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error) + getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) + checkSSHHost func(ctx context.Context, principal, token string) (bool, error) + getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error) + version func() authority.Version +} + +// TODO: remove once Authorize is deprecated. +func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { + return m.AuthorizeSign(ott) +} + +func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { + if m.authorizeSign != nil { + return m.authorizeSign(ott) + } + return m.ret1.([]provisioner.SignOption), m.err +} + +func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions { + if m.getTLSOptions != nil { + return m.getTLSOptions() + } + return m.ret1.(*authority.TLSOptions) +} + +func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) { + if m.root != nil { + return m.root(shasum) + } + return m.ret1.(*x509.Certificate), m.err +} + +func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + if m.sign != nil { + return m.sign(cr, opts, signOpts...) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { + if m.renew != nil { + return m.renew(cert) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { + if m.rekey != nil { + return m.rekey(oldcert, pk) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { + if m.getProvisioners != nil { + return m.getProvisioners(nextCursor, limit) + } + return m.ret1.(provisioner.List), m.ret2.(string), m.err +} + +func (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) { + if m.loadProvisionerByCertificate != nil { + return m.loadProvisionerByCertificate(cert) + } + return m.ret1.(provisioner.Interface), m.err +} + +func (m *mockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { + if m.loadProvisionerByName != nil { + return m.loadProvisionerByName(name) + } + return m.ret1.(provisioner.Interface), m.err +} + +func (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { + if m.revoke != nil { + return m.revoke(ctx, opts) + } + return m.err +} + +func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) { + if m.getEncryptedKey != nil { + return m.getEncryptedKey(kid) + } + return m.ret1.(string), m.err +} + +func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) { + if m.getRoots != nil { + return m.getRoots() + } + return m.ret1.([]*x509.Certificate), m.err +} + +func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) { + if m.getFederation != nil { + return m.getFederation() + } + return m.ret1.([]*x509.Certificate), m.err +} + +func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + if m.signSSH != nil { + return m.signSSH(ctx, key, opts, signOpts...) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) { + if m.signSSHAddUser != nil { + return m.signSSHAddUser(ctx, key, cert) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) { + if m.renewSSH != nil { + return m.renewSSH(ctx, cert) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + if m.rekeySSH != nil { + return m.rekeySSH(ctx, cert, key, signOpts...) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) { + if m.getSSHHosts != nil { + return m.getSSHHosts(ctx, cert) + } + return m.ret1.([]authority.Host), m.err +} + +func (m *mockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) { + if m.getSSHRoots != nil { + return m.getSSHRoots(ctx) + } + return m.ret1.(*authority.SSHKeys), m.err +} + +func (m *mockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) { + if m.getSSHFederation != nil { + return m.getSSHFederation(ctx) + } + return m.ret1.(*authority.SSHKeys), m.err +} + +func (m *mockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { + if m.getSSHConfig != nil { + return m.getSSHConfig(ctx, typ, data) + } + return m.ret1.([]templates.Output), m.err +} + +func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { + if m.checkSSHHost != nil { + return m.checkSSHHost(ctx, principal, token) + } + return m.ret1.(bool), m.err +} + +func (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) { + if m.getSSHBastion != nil { + return m.getSSHBastion(ctx, user, hostname) + } + return m.ret1.(*authority.Bastion), m.err +} + +func (m *mockAuthority) Version() authority.Version { + if m.version != nil { + return m.version() + } + return m.ret1.(authority.Version) +} + func TestNewCertificate(t *testing.T) { cert := parseCertificate(rootPEM) if !reflect.DeepEqual(Certificate{Certificate: cert}, NewCertificate(cert)) { @@ -561,7 +765,7 @@ func Test_caHandler_Route(t *testing.T) { fields fields args args }{ - {"ok", fields{&MockAuthority{}}, args{chi.NewRouter()}}, + {"ok", fields{&mockAuthority{}}, args{chi.NewRouter()}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -576,7 +780,7 @@ func Test_caHandler_Route(t *testing.T) { func Test_caHandler_Health(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com/health", nil) w := httptest.NewRecorder() - h := New(&MockAuthority{}).(*caHandler) + h := New(&mockAuthority{}).(*caHandler) h.Health(w, req) res := w.Result() @@ -616,7 +820,7 @@ func Test_caHandler_Root(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ret1: tt.root, err: tt.err}).(*caHandler) + h := New(&mockAuthority{ret1: tt.root, err: tt.err}).(*caHandler) w := httptest.NewRecorder() h.Root(w, req) res := w.Result() @@ -680,7 +884,7 @@ func Test_caHandler_Sign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.signErr, authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return tt.certAttrOpts, tt.autherr @@ -734,7 +938,7 @@ func Test_caHandler_Renew(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.err, getTLSOptions: func() *authority.TLSOptions { return nil @@ -795,7 +999,7 @@ func Test_caHandler_Rekey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ ret1: tt.cert, ret2: tt.root, err: tt.err, getTLSOptions: func() *authority.TLSOptions { return nil @@ -873,9 +1077,9 @@ func Test_caHandler_Provisioners(t *testing.T) { args args statusCode int }{ - {"ok", fields{&MockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), req}, 200}, - {"fail", fields{&MockAuthority{ret1: p, ret2: "", err: fmt.Errorf("the error")}}, args{httptest.NewRecorder(), req}, 500}, - {"limit fail", fields{&MockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), reqLimitFail}, 400}, + {"ok", fields{&mockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), req}, 200}, + {"fail", fields{&mockAuthority{ret1: p, ret2: "", err: fmt.Errorf("the error")}}, args{httptest.NewRecorder(), req}, 500}, + {"limit fail", fields{&mockAuthority{ret1: p, ret2: ""}}, args{httptest.NewRecorder(), reqLimitFail}, 400}, } expected, err := json.Marshal(pr) @@ -950,8 +1154,8 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { args args statusCode int }{ - {"ok", fields{&MockAuthority{ret1: privKey}}, args{httptest.NewRecorder(), req}, 200}, - {"fail", fields{&MockAuthority{ret1: "", err: fmt.Errorf("not found")}}, args{httptest.NewRecorder(), req}, 404}, + {"ok", fields{&mockAuthority{ret1: privKey}}, args{httptest.NewRecorder(), req}, 200}, + {"fail", fields{&mockAuthority{ret1: "", err: fmt.Errorf("not found")}}, args{httptest.NewRecorder(), req}, 404}, } expected := []byte(`{"key":"` + privKey + `"}`) @@ -1010,7 +1214,7 @@ func Test_caHandler_Roots(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) + h := New(&mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) req := httptest.NewRequest("GET", "http://example.com/roots", nil) req.TLS = tt.tls w := httptest.NewRecorder() @@ -1056,7 +1260,7 @@ func Test_caHandler_Federation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) + h := New(&mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) req := httptest.NewRequest("GET", "http://example.com/federation", nil) req.TLS = tt.tls w := httptest.NewRecorder() diff --git a/api/revoke_test.go b/api/revoke_test.go index b0eaef3d..4ed4e3fe 100644 --- a/api/revoke_test.go +++ b/api/revoke_test.go @@ -106,7 +106,7 @@ func Test_caHandler_Revoke(t *testing.T) { return test{ input: string(input), statusCode: http.StatusOK, - auth: &MockAuthority{ + auth: &mockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, @@ -150,7 +150,7 @@ func Test_caHandler_Revoke(t *testing.T) { input: string(input), statusCode: http.StatusOK, tls: cs, - auth: &MockAuthority{ + auth: &mockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, @@ -185,7 +185,7 @@ func Test_caHandler_Revoke(t *testing.T) { return test{ input: string(input), statusCode: http.StatusInternalServerError, - auth: &MockAuthority{ + auth: &mockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, @@ -207,7 +207,7 @@ func Test_caHandler_Revoke(t *testing.T) { return test{ input: string(input), statusCode: http.StatusForbidden, - auth: &MockAuthority{ + auth: &mockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return nil, nil }, diff --git a/api/ssh_test.go b/api/ssh_test.go index df9e2f45..a3d7da0d 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -314,7 +314,7 @@ func Test_caHandler_SSHSign(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ authorizeSign: func(ott string) ([]provisioner.SignOption, error) { return []provisioner.SignOption{}, tt.authErr }, @@ -377,7 +377,7 @@ func Test_caHandler_SSHRoots(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ getSSHRoots: func(ctx context.Context) (*authority.SSHKeys, error) { return tt.keys, tt.keysErr }, @@ -431,7 +431,7 @@ func Test_caHandler_SSHFederation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ getSSHFederation: func(ctx context.Context) (*authority.SSHKeys, error) { return tt.keys, tt.keysErr }, @@ -491,7 +491,7 @@ func Test_caHandler_SSHConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ getSSHConfig: func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { return tt.output, tt.err }, @@ -538,7 +538,7 @@ func Test_caHandler_SSHCheckHost(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ checkSSHHost: func(ctx context.Context, principal, token string) (bool, error) { return tt.exists, tt.err }, @@ -589,7 +589,7 @@ func Test_caHandler_SSHGetHosts(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ getSSHHosts: func(context.Context, *x509.Certificate) ([]authority.Host, error) { return tt.hosts, tt.err }, @@ -644,7 +644,7 @@ func Test_caHandler_SSHBastion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := New(&MockAuthority{ + h := New(&mockAuthority{ getSSHBastion: func(ctx context.Context, user, hostname string) (*authority.Bastion, error) { return tt.bastion, tt.bastionErr }, diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 48e81ecc..18959acb 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -54,7 +54,7 @@ func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { // provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME // provisioner is set to true and thus has EAB enabled. -func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *admin.Error) { +func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, error) { var ( p provisioner.Interface err error diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 15c581f4..ba956f21 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -17,7 +17,6 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" - "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" @@ -39,7 +38,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { type test struct { ctx context.Context db admin.DB - auth api.LinkedAuthority + auth adminAuthority next nextHTTP err *admin.Error statusCode int @@ -49,7 +48,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return nil, errors.New("force") @@ -68,7 +67,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.MockProvisioner{ @@ -108,7 +107,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.MockProvisioner{ @@ -185,14 +184,14 @@ func TestHandler_requireEABEnabled(t *testing.T) { func TestHandler_provisionerHasEABEnabled(t *testing.T) { type test struct { db admin.DB - auth api.LinkedAuthority + auth adminAuthority provisionerName string want bool err *admin.Error } var tests = map[string]func(t *testing.T) test{ "fail/auth.LoadProvisionerByName": func(t *testing.T) test { - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return nil, errors.New("force") @@ -206,7 +205,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } }, "fail/db.GetProvisioner": func(t *testing.T) test { - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.MockProvisioner{ @@ -231,7 +230,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } }, "fail/prov.GetDetails": func(t *testing.T) test { - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.MockProvisioner{ @@ -260,7 +259,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } }, "fail/details.GetACME": func(t *testing.T) test { - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.MockProvisioner{ @@ -293,7 +292,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } }, "ok/eab-disabled": func(t *testing.T) test { - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "eab-disabled", name) return &provisioner.MockProvisioner{ @@ -327,7 +326,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } }, "ok/eab-enabled": func(t *testing.T) test { - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "eab-enabled", name) return &provisioner.MockProvisioner{ @@ -375,16 +374,13 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { return } if tc.err != nil { - // TODO(hs): the output of the diff seems to be equal to each other; not sure why it's marked as different =/ - // opts := []cmp.Option{cmpopts.EquateErrors()} - // if !cmp.Equal(tc.err, err, opts...) { - // t.Errorf("Handler.provisionerHasEABEnabled() diff =\n%v", cmp.Diff(tc.err, err, opts...)) - // } - assert.Equals(t, tc.err.Type, err.Type) - assert.Equals(t, tc.err.Status, err.Status) - assert.Equals(t, tc.err.StatusCode(), err.StatusCode()) - assert.Equals(t, tc.err.Message, err.Message) - assert.Equals(t, tc.err.Detail, err.Detail) + assert.Type(t, &admin.Error{}, err) + adminError, _ := err.(*admin.Error) + assert.Equals(t, tc.err.Type, adminError.Type) + assert.Equals(t, tc.err.Status, adminError.Status) + assert.Equals(t, tc.err.StatusCode(), adminError.StatusCode()) + assert.Equals(t, tc.err.Message, adminError.Message) + assert.Equals(t, tc.err.Detail, adminError.Detail) return } if got != tc.want { diff --git a/authority/admin/api/admin.go b/authority/admin/api/admin.go index bf79ebcf..7aa66d0f 100644 --- a/authority/admin/api/admin.go +++ b/authority/admin/api/admin.go @@ -1,14 +1,32 @@ package api import ( + "context" "net/http" "github.com/go-chi/chi" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" ) +type adminAuthority interface { + LoadProvisionerByName(string) (provisioner.Interface, error) + GetProvisioners(cursor string, limit int) (provisioner.List, string, error) + IsAdminAPIEnabled() bool + LoadAdminByID(id string) (*linkedca.Admin, bool) + GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) + StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error + UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) + RemoveAdmin(ctx context.Context, id string) error + AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) + StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error + LoadProvisionerByID(id string) (provisioner.Interface, error) + UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error + RemoveProvisioner(ctx context.Context, id string) error +} + // CreateAdminRequest represents the body for a CreateAdmin request. type CreateAdminRequest struct { Subject string `json:"subject"` diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go index da044d58..8d223b52 100644 --- a/authority/admin/api/admin_test.go +++ b/authority/admin/api/admin_test.go @@ -15,13 +15,121 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/smallstep/assert" - "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" "google.golang.org/protobuf/types/known/timestamppb" ) +type mockAdminAuthority struct { + MockLoadProvisionerByName func(name string) (provisioner.Interface, error) + MockGetProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) + MockRet1, MockRet2 interface{} // TODO: refactor the ret1/ret2 into those two + MockErr error + MockIsAdminAPIEnabled func() bool + MockLoadAdminByID func(id string) (*linkedca.Admin, bool) + MockGetAdmins func(cursor string, limit int) ([]*linkedca.Admin, string, error) + MockStoreAdmin func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error + MockUpdateAdmin func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) + MockRemoveAdmin func(ctx context.Context, id string) error + MockAuthorizeAdminToken func(r *http.Request, token string) (*linkedca.Admin, error) + MockStoreProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error + MockLoadProvisionerByID func(id string) (provisioner.Interface, error) + MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error + MockRemoveProvisioner func(ctx context.Context, id string) error +} + +func (m *mockAdminAuthority) IsAdminAPIEnabled() bool { + if m.MockIsAdminAPIEnabled != nil { + return m.MockIsAdminAPIEnabled() + } + return m.MockRet1.(bool) +} + +func (m *mockAdminAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { + if m.MockLoadProvisionerByName != nil { + return m.MockLoadProvisionerByName(name) + } + return m.MockRet1.(provisioner.Interface), m.MockErr +} + +func (m *mockAdminAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { + if m.MockGetProvisioners != nil { + return m.MockGetProvisioners(nextCursor, limit) + } + return m.MockRet1.(provisioner.List), m.MockRet2.(string), m.MockErr +} + +func (m *mockAdminAuthority) LoadAdminByID(id string) (*linkedca.Admin, bool) { + if m.MockLoadAdminByID != nil { + return m.MockLoadAdminByID(id) + } + return m.MockRet1.(*linkedca.Admin), m.MockRet2.(bool) +} + +func (m *mockAdminAuthority) GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) { + if m.MockGetAdmins != nil { + return m.MockGetAdmins(cursor, limit) + } + return m.MockRet1.([]*linkedca.Admin), m.MockRet2.(string), m.MockErr +} + +func (m *mockAdminAuthority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + if m.MockStoreAdmin != nil { + return m.MockStoreAdmin(ctx, adm, prov) + } + return m.MockErr +} + +func (m *mockAdminAuthority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + if m.MockUpdateAdmin != nil { + return m.MockUpdateAdmin(ctx, id, nu) + } + return m.MockRet1.(*linkedca.Admin), m.MockErr +} + +func (m *mockAdminAuthority) RemoveAdmin(ctx context.Context, id string) error { + if m.MockRemoveAdmin != nil { + return m.MockRemoveAdmin(ctx, id) + } + return m.MockErr +} + +func (m *mockAdminAuthority) AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) { + if m.MockAuthorizeAdminToken != nil { + return m.MockAuthorizeAdminToken(r, token) + } + return m.MockRet1.(*linkedca.Admin), m.MockErr +} + +func (m *mockAdminAuthority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { + if m.MockStoreProvisioner != nil { + return m.MockStoreProvisioner(ctx, prov) + } + return m.MockErr +} + +func (m *mockAdminAuthority) LoadProvisionerByID(id string) (provisioner.Interface, error) { + if m.MockLoadProvisionerByID != nil { + return m.MockLoadProvisionerByID(id) + } + return m.MockRet1.(provisioner.Interface), m.MockErr +} + +func (m *mockAdminAuthority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error { + if m.MockUpdateProvisioner != nil { + return m.MockUpdateProvisioner(ctx, nu) + } + return m.MockErr +} + +func (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) error { + if m.MockRemoveProvisioner != nil { + return m.MockRemoveProvisioner(ctx, id) + } + return m.MockErr +} + func TestCreateAdminRequest_Validate(t *testing.T) { type fields struct { Subject string @@ -148,7 +256,7 @@ func TestUpdateAdminRequest_Validate(t *testing.T) { func TestHandler_GetAdmin(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority statusCode int err *admin.Error adm *linkedca.Admin @@ -158,7 +266,7 @@ func TestHandler_GetAdmin(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("id", "adminID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { assert.Equals(t, "adminID", id) return nil, false @@ -191,7 +299,7 @@ func TestHandler_GetAdmin(t *testing.T) { CreatedAt: timestamppb.New(createdAt), DeletedAt: timestamppb.New(deletedAt), } - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { assert.Equals(t, "adminID", id) return adm, true @@ -254,7 +362,7 @@ func TestHandler_GetAdmin(t *testing.T) { func TestHandler_GetAdmins(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority req *http.Request statusCode int err *admin.Error @@ -277,7 +385,7 @@ func TestHandler_GetAdmins(t *testing.T) { }, "fail/auth.GetAdmins": func(t *testing.T) test { req := httptest.NewRequest("GET", "/foo", nil) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { assert.Equals(t, "", cursor) assert.Equals(t, 0, limit) @@ -319,7 +427,7 @@ func TestHandler_GetAdmins(t *testing.T) { CreatedAt: timestamppb.New(createdAt), DeletedAt: timestamppb.New(deletedAt), } - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { assert.Equals(t, "", cursor) assert.Equals(t, 0, limit) @@ -390,7 +498,7 @@ func TestHandler_GetAdmins(t *testing.T) { func TestHandler_CreateAdmin(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority body []byte statusCode int err *admin.Error @@ -439,7 +547,7 @@ func TestHandler_CreateAdmin(t *testing.T) { } body, err := json.Marshal(req) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "prov", name) return nil, errors.New("force") @@ -466,7 +574,7 @@ func TestHandler_CreateAdmin(t *testing.T) { } body, err := json.Marshal(req) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "prov", name) return &provisioner.ACME{ @@ -501,7 +609,7 @@ func TestHandler_CreateAdmin(t *testing.T) { } body, err := json.Marshal(req) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "prov", name) return &provisioner.ACME{ @@ -576,7 +684,7 @@ func TestHandler_CreateAdmin(t *testing.T) { func TestHandler_DeleteAdmin(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority statusCode int err *admin.Error } @@ -585,7 +693,7 @@ func TestHandler_DeleteAdmin(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("id", "adminID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockRemoveAdmin: func(ctx context.Context, id string) error { assert.Equals(t, "adminID", id) return errors.New("force") @@ -607,7 +715,7 @@ func TestHandler_DeleteAdmin(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("id", "adminID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockRemoveAdmin: func(ctx context.Context, id string) error { assert.Equals(t, "adminID", id) return nil @@ -666,7 +774,7 @@ func TestHandler_DeleteAdmin(t *testing.T) { func TestHandler_UpdateAdmin(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority body []byte statusCode int err *admin.Error @@ -714,7 +822,7 @@ func TestHandler_UpdateAdmin(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("id", "adminID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { assert.Equals(t, "adminID", id) assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) @@ -749,7 +857,7 @@ func TestHandler_UpdateAdmin(t *testing.T) { Subject: "admin", Type: linkedca.Admin_SUPER_ADMIN, } - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { assert.Equals(t, "adminID", id) assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index b3ed04bf..fcdb626b 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -9,12 +9,12 @@ import ( // Handler is the Admin API request handler. type Handler struct { db admin.DB - auth api.LinkedAuthority // was: *authority.Authority + auth adminAuthority acmeDB acme.DB } // NewHandler returns a new Authority Config Handler. -func NewHandler(auth api.LinkedAuthority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler { +func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler { return &Handler{ db: adminDB, auth: auth, diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 3231cc6d..7fb4671a 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -13,7 +13,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/smallstep/assert" - "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "go.step.sm/linkedca" "google.golang.org/protobuf/types/known/timestamppb" @@ -22,7 +21,7 @@ import ( func TestHandler_requireAPIEnabled(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority next nextHTTP err *admin.Error statusCode int @@ -31,7 +30,7 @@ func TestHandler_requireAPIEnabled(t *testing.T) { "fail/auth.IsAdminAPIEnabled": func(t *testing.T) test { return test{ ctx: context.Background(), - auth: &api.MockAuthority{ + auth: &mockAdminAuthority{ MockIsAdminAPIEnabled: func() bool { return false }, @@ -46,7 +45,7 @@ func TestHandler_requireAPIEnabled(t *testing.T) { } }, "ok": func(t *testing.T) test { - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockIsAdminAPIEnabled: func() bool { return true }, @@ -101,7 +100,7 @@ func TestHandler_requireAPIEnabled(t *testing.T) { func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority req *http.Request next nextHTTP err *admin.Error @@ -126,7 +125,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { "fail/auth.AuthorizeAdminToken": func(t *testing.T) test { req := httptest.NewRequest("GET", "/foo", nil) req.Header["Authorization"] = []string{"token"} - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { assert.Equals(t, "token", token) return nil, admin.NewError( @@ -162,7 +161,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { CreatedAt: timestamppb.New(createdAt), DeletedAt: timestamppb.New(deletedAt), } - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { assert.Equals(t, "token", token) return admin, nil diff --git a/authority/admin/api/provisioner_test.go b/authority/admin/api/provisioner_test.go index 68a54fe8..6c463590 100644 --- a/authority/admin/api/provisioner_test.go +++ b/authority/admin/api/provisioner_test.go @@ -15,7 +15,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/smallstep/assert" - "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" @@ -26,7 +25,7 @@ import ( func TestHandler_GetProvisioner(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority db admin.DB req *http.Request statusCode int @@ -38,7 +37,7 @@ func TestHandler_GetProvisioner(t *testing.T) { req := httptest.NewRequest("GET", "/foo?id=provID", nil) chiCtx := chi.NewRouteContext() ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) { assert.Equals(t, "provID", id) return nil, errors.New("force") @@ -62,7 +61,7 @@ func TestHandler_GetProvisioner(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("name", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return nil, errors.New("force") @@ -86,7 +85,7 @@ func TestHandler_GetProvisioner(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("name", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.ACME{ @@ -120,7 +119,7 @@ func TestHandler_GetProvisioner(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("name", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.ACME{ @@ -198,7 +197,7 @@ func TestHandler_GetProvisioner(t *testing.T) { func TestHandler_GetProvisioners(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority req *http.Request statusCode int err *admin.Error @@ -221,7 +220,7 @@ func TestHandler_GetProvisioners(t *testing.T) { }, "fail/auth.GetProvisioners": func(t *testing.T) test { req := httptest.NewRequest("GET", "/foo", nil) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) { assert.Equals(t, "", cursor) assert.Equals(t, 0, limit) @@ -255,7 +254,7 @@ func TestHandler_GetProvisioners(t *testing.T) { RequireEAB: false, }, } - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) { assert.Equals(t, "", cursor) assert.Equals(t, 0, limit) @@ -324,7 +323,7 @@ func TestHandler_GetProvisioners(t *testing.T) { func TestHandler_CreateProvisioner(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority body []byte statusCode int err *admin.Error @@ -357,7 +356,7 @@ func TestHandler_CreateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error { assert.Equals(t, "provID", prov.Id) return errors.New("force") @@ -384,7 +383,7 @@ func TestHandler_CreateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error { assert.Equals(t, "provID", prov.Id) return nil @@ -447,7 +446,7 @@ func TestHandler_CreateProvisioner(t *testing.T) { func TestHandler_DeleteProvisioner(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority req *http.Request statusCode int err *admin.Error @@ -457,7 +456,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) { req := httptest.NewRequest("DELETE", "/foo?id=provID", nil) chiCtx := chi.NewRouteContext() ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) { assert.Equals(t, "provID", id) return nil, errors.New("force") @@ -481,7 +480,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("name", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return nil, errors.New("force") @@ -505,7 +504,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("name", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -537,7 +536,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("name", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -604,7 +603,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) { func TestHandler_UpdateProvisioner(t *testing.T) { type test struct { ctx context.Context - auth api.LinkedAuthority + auth adminAuthority body []byte db admin.DB statusCode int @@ -637,13 +636,9 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) - // return &provisioner.OIDC{ - // ID: "provID", - // Name: "provName", - // }, nil return nil, errors.New("force") }, } @@ -671,7 +666,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -711,7 +706,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -754,7 +749,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -799,7 +794,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -847,7 +842,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -898,7 +893,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -952,7 +947,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ @@ -1017,7 +1012,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) { } body, err := protojson.Marshal(prov) assert.FatalError(t, err) - auth := &api.MockAuthority{ + auth := &mockAdminAuthority{ MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { assert.Equals(t, "provName", name) return &provisioner.OIDC{ From 11a7f011778a4f771f838cef4e8635a2e73cea25 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 22 Dec 2021 15:42:49 +0100 Subject: [PATCH 40/47] Simplify lookup cursor logic for ExternalAccountKeys --- acme/db/nosql/account.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index ca51e8ba..1a611ad2 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -282,13 +282,15 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName, curso if dbeak.Provisioner != provisionerName { continue } - // skip the IDs not matching the cursor to look for in the sorted list. - if cursor != "" && !foundCursorKey && cursor != dbeak.ID { - continue - } - // look for the entry pointed to by the cursor (the next item to return), to start selecting items - if cursor != "" && !foundCursorKey && cursor == dbeak.ID { - foundCursorKey = true + // look for the entry pointed to by the cursor (the next item to return) and start selecting items after finding it + if cursor != "" && !foundCursorKey { + if cursor == dbeak.ID { + // from here on, items should be selected for the result. + foundCursorKey = true + } else { + // skip the IDs not matching the cursor to look for. + continue + } } // return if the limit of items was found in the previous iteration; the next cursor is set to the next item to return if len(keys) == limit { From 30859d3c8320e9d76580087548d40157b47a8340 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 6 Jan 2022 14:09:35 +0100 Subject: [PATCH 41/47] Remove server-side paging logic for ExternalAccountKeys --- acme/db.go | 12 ++-- acme/db/nosql/account.go | 33 ++------- acme/db/nosql/account_test.go | 112 ++----------------------------- authority/admin/api/acme.go | 27 +++----- authority/admin/api/acme_test.go | 42 ++---------- ca/adminClient.go | 52 ++++++++++---- 6 files changed, 71 insertions(+), 207 deletions(-) diff --git a/acme/db.go b/acme/db.go index 9df0a221..bed55c85 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,7 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) + GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error @@ -58,7 +58,7 @@ type MockDB struct { MockCreateExternalAccountKey func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - MockGetExternalAccountKeys func(ctx context.Context, provisionerName string, cursor string, limit int) ([]*ExternalAccountKey, string, error) + MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error @@ -149,13 +149,13 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, key } // GetExternalAccountKeys mock -func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) { +func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) { if m.MockGetExternalAccountKeys != nil { - return m.MockGetExternalAccountKeys(ctx, provisionerName, cursor, limit) + return m.MockGetExternalAccountKeys(ctx, provisionerName) } 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/account.go b/acme/db/nosql/account.go index 1a611ad2..d340ec5c 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -258,44 +258,23 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, key } // GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner -func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { + + // TODO: lookup by provisioner based on index entries, err := db.db.List(externalAccountKeyTable) if err != nil { - return nil, "", err - } - - // set sane limits; based on the Admin API limits - switch { - case limit <= 0: - limit = 20 - case limit > 100: - limit = 100 + return nil, err } - foundCursorKey := false keys := []*acme.ExternalAccountKey{} for _, entry := range entries { // entries is sorted alphabetically on the key (ID) of the EAK; no need to sort this again. dbeak := new(dbExternalAccountKey) if err = json.Unmarshal(entry.Value, dbeak); err != nil { - return nil, "", errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) } if dbeak.Provisioner != provisionerName { continue } - // look for the entry pointed to by the cursor (the next item to return) and start selecting items after finding it - if cursor != "" && !foundCursorKey { - if cursor == dbeak.ID { - // from here on, items should be selected for the result. - foundCursorKey = true - } else { - // skip the IDs not matching the cursor to look for. - continue - } - } - // return if the limit of items was found in the previous iteration; the next cursor is set to the next item to return - if len(keys) == limit { - return keys, dbeak.ID, nil - } keys = append(keys, &acme.ExternalAccountKey{ ID: dbeak.ID, KeyBytes: dbeak.KeyBytes, @@ -307,7 +286,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName, curso }) } - return keys, "", nil + return keys, nil } // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 168e93c1..4b94e40f 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -1085,17 +1085,13 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { keyID1 := "keyID1" keyID2 := "keyID2" keyID3 := "keyID3" - keyID4 := "keyID4" prov := "acmeProv" ref := "ref" type test struct { - db nosql.DB - err error - cursor string - nextCursor string - limit int - acmeErr *acme.Error - eaks []*acme.ExternalAccountKey + db nosql.DB + err error + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey } var tests = map[string]func(t *testing.T) test{ "ok": func(t *testing.T) test { @@ -1173,103 +1169,6 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { }, } }, - "ok/paging-single-entry": func(t *testing.T) test { - now := clock.Now() - dbeak1 := &dbExternalAccountKey{ - ID: keyID1, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b1, err := json.Marshal(dbeak1) - assert.FatalError(t, err) - dbeak2 := &dbExternalAccountKey{ - ID: keyID2, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b2, err := json.Marshal(dbeak2) - assert.FatalError(t, err) - dbeak3 := &dbExternalAccountKey{ - ID: keyID3, - Provisioner: "differentProvisioner", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b3, err := json.Marshal(dbeak3) - assert.FatalError(t, err) - dbeak4 := &dbExternalAccountKey{ - ID: keyID4, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b4, err := json.Marshal(dbeak4) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*nosqldb.Entry, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - return []*nosqldb.Entry{ - { - Bucket: bucket, - Key: []byte(keyID1), - Value: b1, - }, - { - Bucket: bucket, - Key: []byte(keyID2), - Value: b2, - }, - { - Bucket: bucket, - Key: []byte(keyID3), - Value: b3, - }, - { - Bucket: bucket, - Key: []byte(keyID4), - Value: b4, - }, - }, nil - }, - }, - cursor: keyID2, - limit: 1, - nextCursor: keyID4, - eaks: []*acme.ExternalAccountKey{ - { - ID: keyID2, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - }, - } - }, - "ok/paging-max-limit": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*nosqldb.Entry, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - return []*nosqldb.Entry{}, nil - }, - }, - limit: 1337, - eaks: []*acme.ExternalAccountKey{}, - } - }, "fail/db.List-error": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ @@ -1304,7 +1203,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), prov, tc.cursor, tc.limit); err != nil { + if eaks, err := d.GetExternalAccountKeys(context.Background(), prov); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1330,7 +1229,6 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) } - assert.Equals(t, nextCursor, tc.nextCursor) } }) } diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 18959acb..dbcca15b 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -30,8 +30,7 @@ func (r *CreateExternalAccountKeyRequest) Validate() error { // GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses type GetExternalAccountKeysResponse struct { - EAKs []*linkedca.EABKey `json:"eaks"` - NextCursor string `json:"nextCursor"` + EAKs []*linkedca.EABKey `json:"eaks"` } // requireEABEnabled is a middleware that ensures ACME EAB is enabled @@ -149,26 +148,19 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques api.JSON(w, &DeleteResponse{Status: "ok"}) } -// GetExternalAccountKeys returns a segment of ACME EAB Keys. +// GetExternalAccountKeys returns ACME EAB Keys. If a reference is specified, +// only the ExternalAccountKey with that reference is returned. Otherwise all +// ExternalAccountKeys in the system for a specific provisioner are returned. func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { prov := chi.URLParam(r, "prov") reference := chi.URLParam(r, "ref") var ( - key *acme.ExternalAccountKey - keys []*acme.ExternalAccountKey - err error - cursor string - nextCursor string - limit int + key *acme.ExternalAccountKey + keys []*acme.ExternalAccountKey + err error ) - 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 - } - if reference != "" { if key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference)) @@ -178,7 +170,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) keys = []*acme.ExternalAccountKey{key} } } else { - if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit); err != nil { + if keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) return } @@ -198,7 +190,6 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) } api.JSON(w, &GetExternalAccountKeysResponse{ - EAKs: eaks, - NextCursor: nextCursor, + EAKs: eaks, }) } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index ba956f21..7162ea98 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -840,23 +840,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { err *admin.Error } var tests = map[string]func(t *testing.T) test{ - "fail/parse-cursor": func(t *testing.T) test { - chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") - req := httptest.NewRequest("GET", "/foo?limit=A", nil) - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - 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") @@ -889,11 +872,9 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { assert.Equals(t, "provName", provisionerName) - assert.Equals(t, "", cursor) - assert.Equals(t, 0, limit) - return nil, "", errors.New("force") + return nil, errors.New("force") }, } return test{ @@ -927,8 +908,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { statusCode: 200, req: req, resp: GetExternalAccountKeysResponse{ - EAKs: []*linkedca.EABKey{}, - NextCursor: "", + EAKs: []*linkedca.EABKey{}, }, db: db, err: nil, @@ -968,7 +948,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { BoundAt: timestamppb.New(boundAt), }, }, - NextCursor: "", }, db: db, err: nil, @@ -983,10 +962,8 @@ 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, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { assert.Equals(t, "provName", provisionerName) - assert.Equals(t, "", cursor) - assert.Equals(t, 0, limit) return []*acme.ExternalAccountKey{ { ID: "eakID1", @@ -1011,7 +988,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { BoundAt: boundAtSet, AccountID: "accountID", }, - }, "nextCursorValue", nil + }, nil }, } return test{ @@ -1043,7 +1020,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { Account: "accountID", }, }, - NextCursor: "nextCursorValue", }, db: db, err: nil, @@ -1058,10 +1034,8 @@ 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, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { assert.Equals(t, "provName", provisionerName) - assert.Equals(t, "eakID1", cursor) - assert.Equals(t, 10, limit) return []*acme.ExternalAccountKey{ { ID: "eakID1", @@ -1086,7 +1060,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { BoundAt: boundAtSet, AccountID: "accountID", }, - }, "eakID4", nil + }, nil }, } return test{ @@ -1118,7 +1092,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { Account: "accountID", }, }, - NextCursor: "eakID4", }, db: db, err: nil, @@ -1166,7 +1139,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { if !cmp.Equal(tc.resp, response, opts...) { t.Errorf("h.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...)) } - }) } } diff --git a/ca/adminClient.go b/ca/adminClient.go index f5e90449..ab7c3bbb 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -560,7 +560,7 @@ retry: return nil } -// GetExternalAccountKeysPaginate returns a page from the the GET /admin/acme/eab request to the CA. +// GetExternalAccountKeysPaginate returns a page from the GET /admin/acme/eab request to the CA. func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { var retried bool o := new(adminOptions) @@ -669,21 +669,45 @@ retry: // 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 ( - cursor = "" - eaks = []*linkedca.EABKey{} - ) - for { - resp, err := c.GetExternalAccountKeysPaginate(provisionerName, reference, WithAdminCursor(cursor), WithAdminLimit(100)) - if err != nil { - return nil, err - } - eaks = append(eaks, resp.EAKs...) - if resp.NextCursor == "" { - return eaks, nil + 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 } - cursor = resp.NextCursor + 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 { From ef16febf409a26d743fa3542934a25e17c170574 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 7 Jan 2022 16:59:55 +0100 Subject: [PATCH 42/47] Refactor ACME EAB queries The ACME EAB keys are now also indexed by the provisioner. This solves part of the issue in which too many EAB keys may be in memory at a given time. --- acme/account.go | 14 +- acme/account_test.go | 20 +- acme/api/account.go | 4 +- acme/api/account_test.go | 96 +++--- acme/db.go | 48 +-- acme/db/nosql/account.go | 271 ++++++++++++---- acme/db/nosql/account_test.go | 518 ++++++++++++++++++------------- acme/db/nosql/nosql.go | 26 +- authority/admin/api/acme.go | 88 ++++-- authority/admin/api/acme_test.go | 234 ++++++-------- 10 files changed, 766 insertions(+), 553 deletions(-) diff --git a/acme/account.go b/acme/account.go index 14a707e9..027d7be1 100644 --- a/acme/account.go +++ b/acme/account.go @@ -45,13 +45,13 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { // ExternalAccountKey is an ACME External Account Binding key. type ExternalAccountKey struct { - ID string `json:"id"` - Provisioner string `json:"provisioner"` - Reference string `json:"reference"` - AccountID string `json:"-"` - KeyBytes []byte `json:"-"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt,omitempty"` + ID string `json:"id"` + ProvisionerID string `json:"provisionerID"` + Reference string `json:"reference"` + AccountID string `json:"-"` + KeyBytes []byte `json:"-"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt,omitempty"` } // AlreadyBound returns whether this EAK is already bound to diff --git a/acme/account_test.go b/acme/account_test.go index 44b815b9..33524d87 100644 --- a/acme/account_test.go +++ b/acme/account_test.go @@ -92,10 +92,10 @@ func TestExternalAccountKey_BindTo(t *testing.T) { { name: "ok", eak: &ExternalAccountKey{ - ID: "eakID", - Provisioner: "prov", - Reference: "ref", - KeyBytes: []byte{1, 3, 3, 7}, + ID: "eakID", + ProvisionerID: "provID", + Reference: "ref", + KeyBytes: []byte{1, 3, 3, 7}, }, acct: &Account{ ID: "accountID", @@ -105,12 +105,12 @@ func TestExternalAccountKey_BindTo(t *testing.T) { { name: "fail/already-bound", eak: &ExternalAccountKey{ - ID: "eakID", - Provisioner: "prov", - Reference: "ref", - KeyBytes: []byte{1, 3, 3, 7}, - AccountID: "someAccountID", - BoundAt: boundAt, + ID: "eakID", + ProvisionerID: "provID", + Reference: "ref", + KeyBytes: []byte{1, 3, 3, 7}, + AccountID: "someAccountID", + BoundAt: boundAt, }, acct: &Account{ ID: "accountID", diff --git a/acme/api/account.go b/acme/api/account.go index 658c40a8..bf478d2a 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -144,7 +144,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { api.WriteError(w, err) return } - if err := h.db.UpdateExternalAccountKey(ctx, prov.Name, eak); err != nil { + if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key")) return } @@ -274,7 +274,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc return nil, acmeErr } - externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.Name, keyID) + 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") diff --git a/acme/api/account_test.go b/acme/api/account_test.go index aa8d44ba..e54e3c1a 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -351,6 +351,7 @@ func TestHandler_NewAccount(t *testing.T) { prov := newProv() escProvName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + provID := prov.GetID() type test struct { db acme.DB @@ -554,11 +555,11 @@ func TestHandler_NewAccount(t *testing.T) { ctx = context.WithValue(ctx, provisionerContextKey, prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) eak := &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), } return test{ db: &acme.MockDB{ @@ -731,11 +732,11 @@ func TestHandler_NewAccount(t *testing.T) { }, MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), }, nil }, MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { @@ -1056,6 +1057,7 @@ 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 @@ -1128,11 +1130,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, }, nil }, }, @@ -1142,11 +1144,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, }, err: nil, } @@ -1492,12 +1494,12 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - CreatedAt: createdAt, - AccountID: "some-account-id", - BoundAt: boundAt, + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + CreatedAt: createdAt, + AccountID: "some-account-id", + BoundAt: boundAt, }, nil }, }, @@ -1549,11 +1551,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 2, 3, 4}, - CreatedAt: time.Now(), + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 2, 3, 4}, + CreatedAt: time.Now(), }, nil }, }, @@ -1607,11 +1609,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), }, nil }, }, @@ -1662,11 +1664,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), }, nil }, }, @@ -1718,11 +1720,11 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { db: &acme.MockDB{ MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: escProvName, - Reference: "testeak", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: time.Now(), + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), }, nil }, }, @@ -1765,7 +1767,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { 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.Provisioner, tc.eak.Provisioner) + 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) diff --git a/acme/db.go b/acme/db.go index bed55c85..708bce9e 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,12 +19,12 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error - CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) - GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) - GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) - DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error - UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error + 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) + 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 CreateNonce(ctx context.Context) (Nonce, error) DeleteNonce(ctx context.Context, nonce Nonce) error @@ -56,12 +56,12 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error - MockCreateExternalAccountKey func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) - MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) - MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) - MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) error - MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error + 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) + 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 MockCreateNonce func(ctx context.Context) (Nonce, error) MockDeleteNonce func(ctx context.Context, nonce Nonce) error @@ -129,9 +129,9 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error { } // CreateExternalAccountKey mock -func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) { if m.MockCreateExternalAccountKey != nil { - return m.MockCreateExternalAccountKey(ctx, provisionerName, reference) + return m.MockCreateExternalAccountKey(ctx, provisionerID, reference) } else if m.MockError != nil { return nil, m.MockError } @@ -139,9 +139,9 @@ func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, } // GetExternalAccountKey mock -func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) { if m.MockGetExternalAccountKey != nil { - return m.MockGetExternalAccountKey(ctx, provisionerName, keyID) + return m.MockGetExternalAccountKey(ctx, provisionerID, keyID) } else if m.MockError != nil { return nil, m.MockError } @@ -149,9 +149,9 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, key } // GetExternalAccountKeys mock -func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error) { if m.MockGetExternalAccountKeys != nil { - return m.MockGetExternalAccountKeys(ctx, provisionerName) + return m.MockGetExternalAccountKeys(ctx, provisionerID) } else if m.MockError != nil { return nil, m.MockError } @@ -159,9 +159,9 @@ func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName str } // GetExternalAccountKeyByReference mock -func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { +func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) { if m.MockGetExternalAccountKeyByReference != nil { - return m.MockGetExternalAccountKeyByReference(ctx, provisionerName, reference) + return m.MockGetExternalAccountKeyByReference(ctx, provisionerID, reference) } else if m.MockError != nil { return nil, m.MockError } @@ -169,9 +169,9 @@ func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provision } // DeleteExternalAccountKey mock -func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error { +func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { if m.MockDeleteExternalAccountKey != nil { - return m.MockDeleteExternalAccountKey(ctx, provisionerName, keyID) + return m.MockDeleteExternalAccountKey(ctx, provisionerID, keyID) } else if m.MockError != nil { return m.MockError } @@ -179,9 +179,9 @@ func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerName, } // UpdateExternalAccountKey mock -func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error { +func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error { if m.MockUpdateExternalAccountKey != nil { - return m.MockUpdateExternalAccountKey(ctx, provisionerName, eak) + return m.MockUpdateExternalAccountKey(ctx, provisionerID, eak) } else if m.MockError != nil { return m.MockError } diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index d340ec5c..d3b9949a 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/json" + "sync" "time" "github.com/pkg/errors" @@ -12,6 +13,9 @@ import ( "go.step.sm/crypto/jose" ) +// Mutex for locking referencesByProvisioner index operations. +var referencesByProvisionerIndexMux sync.Mutex + // dbAccount represents an ACME account. type dbAccount struct { ID string `json:"id"` @@ -28,13 +32,13 @@ func (dba *dbAccount) clone() *dbAccount { } type dbExternalAccountKey struct { - ID string `json:"id"` - Provisioner string `json:"provisioner"` - Reference string `json:"reference"` - AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt"` + ID string `json:"id"` + ProvisionerID string `json:"provisionerID"` + Reference string `json:"reference"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` } type dbExternalAccountKeyReference struct { @@ -170,7 +174,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { } // CreateExternalAccountKey creates a new External Account Binding key with a name -func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { +func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { keyID, err := randID() if err != nil { return nil, err @@ -183,106 +187,125 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, ref } dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: provisionerName, - Reference: reference, - KeyBytes: random, - CreatedAt: clock.Now(), + ID: keyID, + ProvisionerID: provisionerID, + Reference: reference, + KeyBytes: random, + CreatedAt: clock.Now(), } if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { return nil, err } + if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil { + return nil, err + } + if dbeak.Reference != "" { dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ Reference: dbeak.Reference, ExternalAccountKeyID: dbeak.ID, } - if err := db.save(ctx, dbeak.Reference, dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { + if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { return nil, err } } return &acme.ExternalAccountKey{ - ID: dbeak.ID, - Provisioner: dbeak.Provisioner, - Reference: dbeak.Reference, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, nil } // GetExternalAccountKey retrieves an External Account Binding key by KeyID -func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return nil, err } - if dbeak.Provisioner != provisionerName { - return nil, acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + if dbeak.ProvisionerID != provisionerID { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created") } return &acme.ExternalAccountKey{ - ID: dbeak.ID, - Provisioner: dbeak.Provisioner, - Reference: dbeak.Reference, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, }, nil } -func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error { +func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) } - if dbeak.Provisioner != provisionerName { - return errors.New("name of provisioner does not match provisioner for which the EAB key was created") + + if dbeak.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") } + if dbeak.Reference != "" { - err = db.db.Del(externalAccountKeysByReferenceTable, []byte(dbeak.Reference)) - if err != nil { - return errors.Wrapf(err, "error deleting ACME EAB Key Reference with Key ID %s and reference %s", keyID, dbeak.Reference) + if err := db.db.Del(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference) } } - if err = db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { + if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) } + if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil { + return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID) + } + return nil } // GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner -func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { + + // TODO: mutex? - // TODO: lookup by provisioner based on index - entries, err := db.db.List(externalAccountKeyTable) + var eakIDs []string + r, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) if err != nil { - return nil, err + if !nosqlDB.IsErrNotFound(err) { + return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) + } + } else { + if err := json.Unmarshal(r, &eakIDs); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) + } } keys := []*acme.ExternalAccountKey{} - for _, entry := range entries { // entries is sorted alphabetically on the key (ID) of the EAK; no need to sort this again. - dbeak := new(dbExternalAccountKey) - if err = json.Unmarshal(entry.Value, dbeak); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) + for _, eakID := range eakIDs { + if eakID == "" { + continue // shouldn't happen; just in case } - if dbeak.Provisioner != provisionerName { - continue + 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) + } } keys = append(keys, &acme.ExternalAccountKey{ - ID: dbeak.ID, - KeyBytes: dbeak.KeyBytes, - Provisioner: dbeak.Provisioner, - Reference: dbeak.Reference, - AccountID: dbeak.AccountID, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, + ID: eak.ID, + KeyBytes: eak.KeyBytes, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, }) } @@ -290,11 +313,12 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string } // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference -func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { +func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { if reference == "" { return nil, nil } - k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(reference)) + + k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, reference))) if nosqlDB.IsErrNotFound(err) { return nil, acme.ErrNotFound } else if err != nil { @@ -304,28 +328,139 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerN if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) } - return db.GetExternalAccountKey(ctx, provisionerName, dbExternalAccountKeyReference.ExternalAccountKeyID) + + return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) } -func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { +func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { old, err := db.getDBExternalAccountKey(ctx, eak.ID) if err != nil { return err } - if old.Provisioner != provisionerName { - return errors.New("name of provisioner does not match provisioner for which the EAB key was created") + if old.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") + } + + if old.ProvisionerID != eak.ProvisionerID { + return errors.New("cannot change provisioner for an existing ACME EAB Key") + } + + if old.Reference != eak.Reference { + return errors.New("cannot change reference for an existing ACME EAB Key") } nu := dbExternalAccountKey{ - ID: eak.ID, - Provisioner: eak.Provisioner, - Reference: eak.Reference, - AccountID: eak.AccountID, - KeyBytes: eak.KeyBytes, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, + ID: eak.ID, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, } return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) } + +func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMux.Lock() + defer referencesByProvisionerIndexMux.Unlock() + + var eakIDs []string + b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) + } + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + var newEAKIDs []string + newEAKIDs = append(newEAKIDs, eakIDs...) + newEAKIDs = append(newEAKIDs, eakID) + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + if len(eakIDs) == 0 { + _old = nil + } + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + + return nil +} + +func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMux.Lock() + defer referencesByProvisionerIndexMux.Unlock() + + var eakIDs []string + b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading reference IDs for provisioner %s", provisionerID) + } + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + newEAKIDs := removeElement(eakIDs, eakID) + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + switch { + case len(eakIDs) == 0: + _old = nil + case len(newEAKIDs) == 0: + _new = nil + } + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving referenceIDs index for provisioner %s", provisionerID) + } + + return nil +} + +// referenceKey returns a unique key for a reference per provisioner +func referenceKey(provisionerID, reference string) string { + return provisionerID + "." + reference +} + +// sliceIndex finds the index of item in slice +func sliceIndex(slice []string, item string) int { + for i := range slice { + if slice[i] == item { + return i + } + } + return -1 +} + +// removeElement deletes the item if it exists in the +// slice. It returns a new slice, keeping the old one intact. +func removeElement(slice []string, item string) []string { + + newSlice := make([]string, 0) + index := sliceIndex(slice, item) + if index < 0 { + newSlice = append(newSlice, slice...) + return newSlice + } + + newSlice = append(newSlice, slice[:index]...) + + return append(newSlice, slice[index+1:]...) +} diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 4b94e40f..77937c2e 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -3,6 +3,7 @@ package nosql import ( "context" "encoding/json" + "fmt" "testing" "time" @@ -307,7 +308,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) { assert.Equals(t, string(key), accID) return nil, errors.New("force") default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, errors.New("force") } }, @@ -340,7 +341,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) { assert.Equals(t, string(key), accID) return b, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, errors.New("force") } }, @@ -462,7 +463,7 @@ func TestDB_CreateAccount(t *testing.T) { assert.True(t, dbacc.DeactivatedAt.IsZero()) return nil, false, errors.New("force") default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, false, errors.New("force") } }, @@ -506,7 +507,7 @@ func TestDB_CreateAccount(t *testing.T) { assert.True(t, dbacc.DeactivatedAt.IsZero()) return nu, true, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, false, errors.New("force") } }, @@ -699,6 +700,7 @@ func TestDB_UpdateAccount(t *testing.T) { func TestDB_getDBExternalAccountKey(t *testing.T) { keyID := "keyID" + provID := "provID" type test struct { db nosql.DB err error @@ -709,12 +711,12 @@ func TestDB_getDBExternalAccountKey(t *testing.T) { "ok": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: "prov", - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b, err := json.Marshal(dbeak) assert.FatalError(t, err) @@ -790,7 +792,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) { } else if assert.Nil(t, tc.err) { assert.Equals(t, dbeak.ID, tc.dbeak.ID) assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) - assert.Equals(t, dbeak.Provisioner, tc.dbeak.Provisioner) + assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID) assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) @@ -802,7 +804,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) { func TestDB_GetExternalAccountKey(t *testing.T) { keyID := "keyID" - prov := "acmeProv" + provID := "provID" type test struct { db nosql.DB err error @@ -813,12 +815,12 @@ func TestDB_GetExternalAccountKey(t *testing.T) { "ok": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b, err := json.Marshal(dbeak) assert.FatalError(t, err) @@ -831,12 +833,12 @@ func TestDB_GetExternalAccountKey(t *testing.T) { }, }, eak: &acme.ExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, }, } }, @@ -856,12 +858,12 @@ func TestDB_GetExternalAccountKey(t *testing.T) { "fail/non-matching-provisioner": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: "aDifferentProv", - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b, err := json.Marshal(dbeak) assert.FatalError(t, err) @@ -874,14 +876,14 @@ func TestDB_GetExternalAccountKey(t *testing.T) { }, }, eak: &acme.ExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, }, - acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), + acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"), } }, } @@ -889,7 +891,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - if eak, err := d.GetExternalAccountKey(context.Background(), prov, keyID); err != nil { + if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -907,7 +909,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { } else if assert.Nil(t, tc.err) { assert.Equals(t, eak.ID, tc.eak.ID) assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) assert.Equals(t, eak.Reference, tc.eak.Reference) assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) assert.Equals(t, eak.AccountID, tc.eak.AccountID) @@ -919,7 +921,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) { func TestDB_GetExternalAccountKeyByReference(t *testing.T) { keyID := "keyID" - prov := "acmeProv" + provID := "provID" ref := "ref" type test struct { db nosql.DB @@ -932,12 +934,12 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { "ok": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ Reference: ref, @@ -953,24 +955,24 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return dbrefBytes, nil case string(externalAccountKeyTable): assert.Equals(t, string(key), keyID) return b, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, errors.New("force") } }, }, eak: &acme.ExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, }, err: nil, } @@ -988,7 +990,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { db: &db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return nil, nosqldb.ErrNotFound }, }, @@ -1001,7 +1003,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { db: &db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return nil, errors.New("force") }, }, @@ -1014,7 +1016,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { db: &db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return []byte{0}, nil }, }, @@ -1034,13 +1036,13 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return dbrefBytes, nil case string(externalAccountKeyTable): assert.Equals(t, string(key), keyID) return nil, errors.New("force") default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, errors.New("force") } }, @@ -1053,7 +1055,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - if eak, err := d.GetExternalAccountKeyByReference(context.Background(), prov, tc.ref); err != nil { + if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1074,7 +1076,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) assert.Equals(t, eak.Reference, tc.eak.Reference) } }) @@ -1085,7 +1087,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { keyID1 := "keyID1" keyID2 := "keyID2" keyID3 := "keyID3" - prov := "acmeProv" + provID := "provID" ref := "ref" type test struct { db nosql.DB @@ -1097,105 +1099,147 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { "ok": func(t *testing.T) test { now := clock.Now() dbeak1 := &dbExternalAccountKey{ - ID: keyID1, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b1, err := json.Marshal(dbeak1) assert.FatalError(t, err) dbeak2 := &dbExternalAccountKey{ - ID: keyID2, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b2, err := json.Marshal(dbeak2) assert.FatalError(t, err) dbeak3 := &dbExternalAccountKey{ - ID: keyID3, - Provisioner: "differentProvisioner", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID3, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b3, err := json.Marshal(dbeak3) assert.FatalError(t, err) return test{ db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + switch string(key) { + case keyID1: + return b1, nil + case keyID2: + return b2, nil + default: + assert.FatalError(t, errors.Errorf("unexpected key %s", string(key))) + return nil, errors.New("force") + } + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + // TODO: remove the MList MList: func(bucket []byte) ([]*nosqldb.Entry, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - return []*nosqldb.Entry{ - { - Bucket: bucket, - Key: []byte(keyID1), - Value: b1, - }, - { - Bucket: bucket, - Key: []byte(keyID2), - Value: b2, - }, - { - Bucket: bucket, - Key: []byte(keyID3), - Value: b3, - }, - }, nil + switch string(bucket) { + case string(externalAccountKeyTable): + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: b1, + }, + { + Bucket: bucket, + Key: []byte(keyID2), + Value: b2, + }, + { + Bucket: bucket, + Key: []byte(keyID3), + Value: b3, + }, + }, nil + case string(externalAccountKeysByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(provID), + Value: b, + }, + }, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } }, }, eaks: []*acme.ExternalAccountKey{ { - ID: keyID1, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, }, { - ID: keyID2, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, }, }, } }, - "fail/db.List-error": func(t *testing.T) test { + "fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*nosqldb.Entry, error) { - assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) return nil, errors.New("force") }, }, - err: errors.New("force"), + err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"), } }, - "fail/unmarshal-error": func(t *testing.T) test { + "fail/db.getDBExternalAccountKey": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*nosqldb.Entry, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - return []*nosqldb.Entry{ - { - Bucket: bucket, - Key: []byte(keyID1), - Value: []byte("foo"), - }, - }, nil + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force bucket") + } }, }, - eaks: []*acme.ExternalAccountKey{}, - err: errors.Errorf("error unmarshaling external account key %s into ExternalAccountKey", keyID1), + err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"), } }, } @@ -1203,7 +1247,7 @@ 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(), prov); err != nil { + if eaks, err := d.GetExternalAccountKeys(context.Background(), provID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1215,7 +1259,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { } default: if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) + assert.Equals(t, tc.err.Error(), err.Error()) } } } else if assert.Nil(t, tc.err) { @@ -1223,7 +1267,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { for i, eak := range eaks { assert.Equals(t, eak.ID, tc.eaks[i].ID) assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) - assert.Equals(t, eak.Provisioner, tc.eaks[i].Provisioner) + assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID) assert.Equals(t, eak.Reference, tc.eaks[i].Reference) assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) @@ -1236,7 +1280,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { func TestDB_DeleteExternalAccountKey(t *testing.T) { keyID := "keyID" - prov := "acmeProv" + provID := "provID" ref := "ref" type test struct { db nosql.DB @@ -1247,12 +1291,12 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { "ok": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ Reference: ref, @@ -1267,27 +1311,46 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return dbrefBytes, nil case string(externalAccountKeyTable): assert.Equals(t, string(key), keyID) return b, nil + case string(externalAccountKeysByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + b, err := json.Marshal([]string{keyID}) + assert.FatalError(t, err) + return b, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) - return nil, errors.New("force") + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") } }, MDel: func(bucket, key []byte) error { switch string(bucket) { case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return nil case string(externalAccountKeyTable): assert.Equals(t, string(key), keyID) return nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) - return errors.New("force") + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) { + fmt.Println(string(bucket)) + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + return nil, true, nil + case string(externalAccountKeysByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nil, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") } }, }, @@ -1302,18 +1365,18 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { return nil, nosqldb.ErrNotFound }, }, - err: errors.New("error loading ACME EAB Key with Key ID keyID"), + err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"), } }, "fail/non-matching-provisioner": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: "differentProvisioner", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b, err := json.Marshal(dbeak) assert.FatalError(t, err) @@ -1325,18 +1388,18 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { return b, nil }, }, - err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), } }, "fail/delete-reference": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ Reference: ref, @@ -1357,36 +1420,36 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { assert.Equals(t, string(key), keyID) return b, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) - return nil, errors.New("force") + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") } }, MDel: func(bucket, key []byte) error { switch string(bucket) { case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return errors.New("force") case string(externalAccountKeyTable): assert.Equals(t, string(key), keyID) return nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) - return errors.New("force") + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") } }, }, - err: errors.New("error deleting ACME EAB Key Reference with Key ID keyID and reference ref"), + err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"), } }, "fail/delete-eak": func(t *testing.T) test { now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } dbref := &dbExternalAccountKeyReference{ Reference: ref, @@ -1407,25 +1470,25 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { assert.Equals(t, string(key), keyID) return b, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, errors.New("force") } }, MDel: func(bucket, key []byte) error { switch string(bucket) { case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) + assert.Equals(t, string(key), provID+"."+ref) return nil case string(externalAccountKeyTable): assert.Equals(t, string(key), keyID) return errors.New("force") default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return errors.New("force") } }, }, - err: errors.New("error deleting ACME EAB Key with Key ID keyID"), + err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"), } }, } @@ -1433,7 +1496,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - if err := d.DeleteExternalAccountKey(context.Background(), prov, keyID); err != nil { + if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1445,7 +1508,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { } default: if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) + assert.Equals(t, err.Error(), tc.err.Error()) } } } else { @@ -1457,7 +1520,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { func TestDB_CreateExternalAccountKey(t *testing.T) { keyID := "keyID" - prov := "acmeProv" + provID := "provID" ref := "ref" type test struct { db nosql.DB @@ -1473,30 +1536,38 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { ) now := clock.Now() eak := &acme.ExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: "ref", - AccountID: "", - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + CreatedAt: now, } return test{ db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - switch string(bucket) { + case string(externalAccountKeysByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) - assert.Equals(t, old, nil) + assert.Equals(t, provID+"."+ref, string(key)) + assert.Equals(t, nil, old) return nu, true, nil case string(externalAccountKeyTable): - assert.Equals(t, old, nil) + assert.Equals(t, nil, old) id = string(key) dbeak := new(dbExternalAccountKey) assert.FatalError(t, json.Unmarshal(nu, dbeak)) assert.Equals(t, string(key), dbeak.ID) - assert.Equals(t, eak.Provisioner, dbeak.Provisioner) + assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID) assert.Equals(t, eak.Reference, dbeak.Reference) assert.Equals(t, 32, len(dbeak.KeyBytes)) assert.False(t, dbeak.CreatedAt.IsZero()) @@ -1504,8 +1575,8 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { assert.True(t, dbeak.BoundAt.IsZero()) return nu, true, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) - return nil, false, errors.New("force") + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") } }, }, @@ -1527,34 +1598,42 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { assert.Equals(t, old, nil) return nu, true, errors.New("force") default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, false, errors.New("force") } }, }, - err: errors.New("error saving acme external_account_key"), + err: errors.New("error saving acme external_account_key: force"), } }, "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - switch string(bucket) { + case string(externalAccountKeysByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) + assert.Equals(t, provID+"."+ref, string(key)) assert.Equals(t, old, nil) return nu, true, errors.New("force") case string(externalAccountKeyTable): assert.Equals(t, old, nil) return nu, true, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, false, errors.New("force") } }, }, - err: errors.New("error saving acme external_account_key"), + err: errors.New("error saving acme external_account_key_reference: force"), } }, } @@ -1562,14 +1641,15 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - eak, err := d.CreateExternalAccountKey(context.Background(), prov, ref) + eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref) + fmt.Println(name, err) if err != nil { if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) + assert.Equals(t, err.Error(), tc.err.Error()) } } else if assert.Nil(t, tc.err) { assert.Equals(t, *tc._id, eak.ID) - assert.Equals(t, prov, eak.Provisioner) + assert.Equals(t, provID, eak.ProvisionerID) assert.Equals(t, ref, eak.Reference) assert.Equals(t, "", eak.AccountID) assert.False(t, eak.CreatedAt.IsZero()) @@ -1582,16 +1662,16 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { func TestDB_UpdateExternalAccountKey(t *testing.T) { keyID := "keyID" - prov := "acmeProv" + provID := "provID" ref := "ref" now := clock.Now() dbeak := &dbExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b, err := json.Marshal(dbeak) assert.FatalError(t, err) @@ -1604,12 +1684,12 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { "ok": func(t *testing.T) test { eak := &acme.ExternalAccountKey{ - ID: keyID, - Provisioner: prov, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } return test{ eak: eak, @@ -1627,7 +1707,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { dbNew := new(dbExternalAccountKey) assert.FatalError(t, json.Unmarshal(nu, dbNew)) assert.Equals(t, dbNew.ID, dbeak.ID) - assert.Equals(t, dbNew.Provisioner, dbeak.Provisioner) + assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID) assert.Equals(t, dbNew.Reference, dbeak.Reference) assert.Equals(t, dbNew.AccountID, dbeak.AccountID) assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) @@ -1640,12 +1720,12 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { }, "fail/provisioner-mismatch": func(t *testing.T) test { newDBEAK := &dbExternalAccountKey{ - ID: keyID, - Provisioner: "differentProvisioner", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, } b, err := json.Marshal(newDBEAK) assert.FatalError(t, err) @@ -1661,7 +1741,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { return b, nil }, }, - err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), } }, "fail/db.Get-error": func(t *testing.T) test { @@ -1685,13 +1765,13 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - if err := d.UpdateExternalAccountKey(context.Background(), prov, tc.eak); err != nil { + if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else if assert.Nil(t, tc.err) { assert.Equals(t, dbeak.ID, tc.eak.ID) - assert.Equals(t, dbeak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID) assert.Equals(t, dbeak.Reference, tc.eak.Reference) assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 3c7019e2..8343c196 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -11,17 +11,18 @@ import ( ) var ( - accountTable = []byte("acme_accounts") - accountByKeyIDTable = []byte("acme_keyID_accountID_index") - authzTable = []byte("acme_authzs") - challengeTable = []byte("acme_challenges") - nonceTable = []byte("nonces") - orderTable = []byte("acme_orders") - ordersByAccountIDTable = []byte("acme_account_orders_index") - certTable = []byte("acme_certs") - certBySerialTable = []byte("acme_serial_certs_index") - externalAccountKeyTable = []byte("acme_external_account_keys") - externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") + accountTable = []byte("acme_accounts") + accountByKeyIDTable = []byte("acme_keyID_accountID_index") + authzTable = []byte("acme_authzs") + challengeTable = []byte("acme_challenges") + nonceTable = []byte("nonces") + orderTable = []byte("acme_orders") + ordersByAccountIDTable = []byte("acme_account_orders_index") + certTable = []byte("acme_certs") + certBySerialTable = []byte("acme_serial_certs_index") + externalAccountKeyTable = []byte("acme_external_account_keys") + externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") + externalAccountKeysByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") ) // DB is a struct that implements the AcmeDB interface. @@ -33,7 +34,8 @@ type DB struct { func New(db nosqlDB.DB) (*DB, error) { tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, challengeTable, nonceTable, orderTable, ordersByAccountIDTable, - certTable, certBySerialTable, externalAccountKeyTable, externalAccountKeysByReferenceTable, + certTable, certBySerialTable, externalAccountKeyTable, + externalAccountKeysByReferenceTable, externalAccountKeysByProvisionerIDTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index dbcca15b..82850010 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -15,6 +15,11 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +const ( + // provisionerContextKey provisioner key + provisionerContextKey = ContextKey("provisioner") +) + // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests type CreateExternalAccountKeyRequest struct { Reference string `json:"reference"` @@ -37,47 +42,63 @@ type GetExternalAccountKeysResponse struct { // before serving requests that act on ACME EAB credentials. func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - prov := chi.URLParam(r, "prov") - eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), prov) + ctx := r.Context() + provName := chi.URLParam(r, "prov") + eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName) if err != nil { api.WriteError(w, err) return } if !eabEnabled { - api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov)) + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov.GetName())) return } - next(w, r) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + next(w, r.WithContext(ctx)) } } // provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME // provisioner is set to true and thus has EAB enabled. -func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, error) { +func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *linkedca.Provisioner, error) { var ( p provisioner.Interface err error ) if p, err = h.auth.LoadProvisionerByName(provisionerName); err != nil { - return false, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName) + return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName) } prov, err := h.db.GetProvisioner(ctx, p.GetID()) if err != nil { - return false, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID()) + return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID()) } details := prov.GetDetails() if details == nil { - return false, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID()) + return false, nil, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID()) } acmeProvisioner := details.GetACME() if acmeProvisioner == nil { - return false, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) + return false, nil, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) } - return acmeProvisioner.GetRequireEab(), nil + return acmeProvisioner.GetRequireEab(), prov, nil +} + +// provisionerFromContext searches the context for a provisioner. Returns the +// provisioner or an error. +func provisionerFromContext(ctx context.Context) (*linkedca.Provisioner, error) { + val := ctx.Value(provisionerContextKey) + if val == nil { + return nil, admin.NewErrorISE("provisioner expected in request context") + } + pval, ok := val.(*linkedca.Provisioner) + if !ok || pval == nil { + return nil, admin.NewErrorISE("provisioner in context is not a linkedca.Provisioner") + } + return pval, nil } // CreateExternalAccountKey creates a new External Account Binding key @@ -93,12 +114,17 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques return } - prov := chi.URLParam(r, "prov") - reference := body.Reference + ctx := r.Context() + prov, err := provisionerFromContext(ctx) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context")) + return + } // check if a key with the reference does not exist (only when a reference was in the request) + reference := body.Reference if reference != "" { - k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) + k, err := h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference) // retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for, // but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound. if shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound); shouldWriteError { @@ -107,7 +133,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques } // if a key was found, return HTTP 409 conflict if k != nil { - err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner %s with reference %s already exists", prov, reference) + err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner '%s' with reference '%s' already exists", prov.GetName(), reference) err.Status = 409 api.WriteError(w, err) return @@ -115,9 +141,9 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques // continue execution if no key was found for the reference } - eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), prov, reference) + eak, err := h.acmeDB.CreateExternalAccountKey(ctx, prov.GetId(), reference) if err != nil { - msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov) + msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov.GetName()) if reference != "" { msg += fmt.Sprintf(" and reference '%s'", reference) } @@ -128,7 +154,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques response := &linkedca.EABKey{ Id: eak.ID, HmacKey: eak.KeyBytes, - Provisioner: eak.Provisioner, + Provisioner: prov.GetName(), Reference: eak.Reference, } @@ -137,10 +163,17 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques // DeleteExternalAccountKey deletes an ACME External Account Key. func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { - prov := chi.URLParam(r, "prov") + keyID := chi.URLParam(r, "id") - if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), prov, keyID); err != nil { + ctx := r.Context() + prov, err := provisionerFromContext(ctx) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context")) + return + } + + if err := h.acmeDB.DeleteExternalAccountKey(ctx, prov.GetId(), keyID); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key '%s'", keyID)) return } @@ -152,8 +185,6 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques // only the ExternalAccountKey with that reference is returned. Otherwise all // ExternalAccountKeys in the system for a specific provisioner are returned. func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { - prov := chi.URLParam(r, "prov") - reference := chi.URLParam(r, "ref") var ( key *acme.ExternalAccountKey @@ -161,8 +192,16 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) err error ) + ctx := r.Context() + prov, err := provisionerFromContext(ctx) + if err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context")) + return + } + + reference := chi.URLParam(r, "ref") if reference != "" { - if key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference); err != nil { + 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)) return } @@ -170,18 +209,19 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) keys = []*acme.ExternalAccountKey{key} } } else { - if keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov); err != nil { + if keys, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId()); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) return } } + provisionerName := prov.GetName() eaks := make([]*linkedca.EABKey, len(keys)) for i, k := range keys { eaks[i] = &linkedca.EABKey{ Id: k.ID, HmacKey: []byte{}, - Provisioner: k.Provisioner, + Provisioner: provisionerName, Reference: k.Reference, Account: k.AccountID, CreatedAt: timestamppb.New(k.CreatedAt), diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 7162ea98..f7de9290 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -368,12 +368,13 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { auth: tc.auth, acmeDB: nil, } - got, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName) + got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName) if (err != nil) != (tc.err != nil) { t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err) return } if tc.err != nil { + assert.Type(t, &linkedca.Provisioner{}, prov) assert.Type(t, &admin.Error{}, err) adminError, _ := err.(*admin.Error) assert.Equals(t, tc.err.Type, adminError.Type) @@ -434,6 +435,10 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { } func TestHandler_CreateExternalAccountKey(t *testing.T) { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + } type test struct { ctx context.Context db acme.DB @@ -487,14 +492,15 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ Reference: "an-external-key-reference", } body, err := json.Marshal(req) assert.FatalError(t, err) db := &acme.MockDB{ - MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return nil, errors.New("force") }, @@ -517,22 +523,23 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ Reference: "an-external-key-reference", } body, err := json.Marshal(req) assert.FatalError(t, err) db := &acme.MockDB{ - MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) past := time.Now().Add(-24 * time.Hour) return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: "provName", - Reference: "an-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: past, + ID: "eakID", + ProvisionerID: "provID", + Reference: "an-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: past, }, nil }, } @@ -546,7 +553,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { Type: admin.ErrorBadRequestType.String(), Status: 409, Detail: "bad request", - Message: "an ACME EAB key for provisioner provName with reference an-external-key-reference already exists", + Message: "an ACME EAB key for provisioner 'provName' with reference 'an-external-key-reference' already exists", }, } }, @@ -554,14 +561,15 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ Reference: "", } body, err := json.Marshal(req) assert.FatalError(t, err) db := &acme.MockDB{ - MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "", reference) return nil, errors.New("force") }, @@ -583,19 +591,20 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ Reference: "an-external-key-reference", } body, err := json.Marshal(req) assert.FatalError(t, err) db := &acme.MockDB{ - MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict }, - MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return nil, errors.New("force") }, @@ -617,6 +626,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ Reference: "", } @@ -624,15 +634,15 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { assert.FatalError(t, err) now := time.Now() db := &acme.MockDB{ - MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "", reference) return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: "provName", - Reference: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: "eakID", + ProvisionerID: "provID", + Reference: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, }, nil }, } @@ -653,6 +663,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) req := CreateExternalAccountKeyRequest{ Reference: "an-external-key-reference", } @@ -660,20 +671,20 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { assert.FatalError(t, err) now := time.Now() db := &acme.MockDB{ - MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict }, - MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: "provName", - Reference: "an-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, + ID: "eakID", + ProvisionerID: "provID", + Reference: "an-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, }, nil }, } @@ -737,6 +748,10 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { } func TestHandler_DeleteExternalAccountKey(t *testing.T) { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + } type test struct { ctx context.Context db acme.DB @@ -749,9 +764,10 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("id", "keyID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) db := &acme.MockDB{ - MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { - assert.Equals(t, "provName", provisionerName) + MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "keyID", keyID) return errors.New("force") }, @@ -773,9 +789,10 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("id", "keyID") ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) db := &acme.MockDB{ - MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { - assert.Equals(t, "provName", provisionerName) + MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "keyID", keyID) return nil }, @@ -831,6 +848,10 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { } func TestHandler_GetExternalAccountKeys(t *testing.T) { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + } type test struct { ctx context.Context db acme.DB @@ -846,9 +867,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { chiCtx.URLParams.Add("ref", "an-external-key-reference") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) db := &acme.MockDB{ - MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return nil, errors.New("force") }, @@ -871,9 +893,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { chiCtx.URLParams.Add("prov", "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, provisionerName string) ([]*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) return nil, errors.New("force") }, } @@ -896,9 +919,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { chiCtx.URLParams.Add("ref", "an-external-key-reference") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) db := &acme.MockDB{ - MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return nil, nil // returning nil; no key found }, @@ -920,17 +944,18 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { chiCtx.URLParams.Add("ref", "an-external-key-reference") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) createdAt := time.Now().Add(-24 * time.Hour) var boundAt time.Time db := &acme.MockDB{ - MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) assert.Equals(t, "an-external-key-reference", reference) return &acme.ExternalAccountKey{ - ID: "eakID", - Provisioner: "provName", - Reference: "an-external-key-reference", - CreatedAt: createdAt, + ID: "eakID", + ProvisionerID: "provID", + Reference: "an-external-key-reference", + CreatedAt: createdAt, }, nil }, } @@ -958,107 +983,36 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { chiCtx.URLParams.Add("prov", "provName") req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + ctx = context.WithValue(ctx, provisionerContextKey, prov) createdAt := time.Now().Add(-24 * time.Hour) var boundAt time.Time boundAtSet := time.Now().Add(-12 * time.Hour) db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) - return []*acme.ExternalAccountKey{ - { - ID: "eakID1", - Provisioner: "provName", - Reference: "some-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, - }, - { - ID: "eakID2", - Provisioner: "provName", - Reference: "some-other-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt.Add(1 * time.Hour), - }, - { - ID: "eakID3", - Provisioner: "provName", - Reference: "another-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, - BoundAt: boundAtSet, - AccountID: "accountID", - }, - }, nil - }, - } - return test{ - ctx: ctx, - statusCode: 200, - req: req, - resp: GetExternalAccountKeysResponse{ - EAKs: []*linkedca.EABKey{ - { - Id: "eakID1", - Provisioner: "provName", - Reference: "some-external-key-reference", - CreatedAt: timestamppb.New(createdAt), - BoundAt: timestamppb.New(boundAt), - }, - { - Id: "eakID2", - Provisioner: "provName", - Reference: "some-other-external-key-reference", - CreatedAt: timestamppb.New(createdAt.Add(1 * time.Hour)), - BoundAt: timestamppb.New(boundAt), - }, - { - Id: "eakID3", - Provisioner: "provName", - Reference: "another-external-key-reference", - CreatedAt: timestamppb.New(createdAt), - BoundAt: timestamppb.New(boundAtSet), - Account: "accountID", - }, - }, - }, - db: db, - err: nil, - } - }, - "ok/multiple-keys-with-cursor-and-limit": func(t *testing.T) test { - chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") - req := httptest.NewRequest("GET", "/foo?cursor=eakID1&limit=10", nil) - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - createdAt := time.Now().Add(-24 * time.Hour) - var boundAt time.Time - boundAtSet := time.Now().Add(-12 * time.Hour) - db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { - assert.Equals(t, "provName", provisionerName) + MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { + assert.Equals(t, "provID", provisionerID) return []*acme.ExternalAccountKey{ { - ID: "eakID1", - Provisioner: "provName", - Reference: "some-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, + ID: "eakID1", + ProvisionerID: "provID", + Reference: "some-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, }, { - ID: "eakID2", - Provisioner: "provName", - Reference: "some-other-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt.Add(1 * time.Hour), + ID: "eakID2", + ProvisionerID: "provID", + Reference: "some-other-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt.Add(1 * time.Hour), }, { - ID: "eakID3", - Provisioner: "provName", - Reference: "another-external-key-reference", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: createdAt, - BoundAt: boundAtSet, - AccountID: "accountID", + ID: "eakID3", + ProvisionerID: "provID", + Reference: "another-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + BoundAt: boundAtSet, + AccountID: "accountID", }, }, nil }, From c0eb420806058a198a6a4daaa7e8fbafe7aa1f77 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 20 Jan 2022 11:03:49 +0100 Subject: [PATCH 43/47] Remove special case for empty slices --- acme/db/nosql/account.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index d3b9949a..4cff6653 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -387,10 +387,6 @@ func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { _new interface{} = newEAKIDs ) - if len(eakIDs) == 0 { - _old = nil - } - if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) } @@ -420,13 +416,6 @@ func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) erro _new interface{} = newEAKIDs ) - switch { - case len(eakIDs) == 0: - _old = nil - case len(newEAKIDs) == 0: - _new = nil - } - if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { return errors.Wrapf(err, "error saving referenceIDs index for provisioner %s", provisionerID) } From 868cc4ad7f501dc8222550e82326a2b2e7077ab3 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 20 Jan 2022 17:06:23 +0100 Subject: [PATCH 44/47] Increase test coverage for additional indexes --- acme/db/nosql/account.go | 282 ----- acme/db/nosql/account_test.go | 1085 ------------------- acme/db/nosql/eab.go | 306 ++++++ acme/db/nosql/eab_test.go | 1710 ++++++++++++++++++++++++++++++ acme/db/nosql/nosql.go | 26 +- authority/admin/api/acme_test.go | 102 ++ 6 files changed, 2131 insertions(+), 1380 deletions(-) create mode 100644 acme/db/nosql/eab.go create mode 100644 acme/db/nosql/eab_test.go diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 4cff6653..603e3f2d 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -2,7 +2,6 @@ package nosql import ( "context" - "crypto/rand" "encoding/json" "sync" "time" @@ -172,284 +171,3 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { return db.save(ctx, old.ID, nu, old, "account", accountTable) } - -// CreateExternalAccountKey creates a new External Account Binding key with a name -func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { - keyID, err := randID() - if err != nil { - return nil, err - } - - random := make([]byte, 32) - _, err = rand.Read(random) - if err != nil { - return nil, err - } - - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provisionerID, - Reference: reference, - KeyBytes: random, - CreatedAt: clock.Now(), - } - - if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { - return nil, err - } - - if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil { - return nil, err - } - - if dbeak.Reference != "" { - dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ - Reference: dbeak.Reference, - ExternalAccountKeyID: dbeak.ID, - } - if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { - return nil, err - } - } - - return &acme.ExternalAccountKey{ - ID: dbeak.ID, - ProvisionerID: dbeak.ProvisionerID, - Reference: dbeak.Reference, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, - }, nil -} - -// GetExternalAccountKey retrieves an External Account Binding key by KeyID -func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { - dbeak, err := db.getDBExternalAccountKey(ctx, keyID) - if err != nil { - return nil, err - } - - if dbeak.ProvisionerID != provisionerID { - return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created") - } - - return &acme.ExternalAccountKey{ - ID: dbeak.ID, - ProvisionerID: dbeak.ProvisionerID, - Reference: dbeak.Reference, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, - }, nil -} - -func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { - dbeak, err := db.getDBExternalAccountKey(ctx, keyID) - if err != nil { - return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) - } - - if dbeak.ProvisionerID != provisionerID { - return errors.New("provisioner does not match provisioner for which the EAB key was created") - } - - if dbeak.Reference != "" { - if err := db.db.Del(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { - return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference) - } - } - if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { - return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) - } - if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil { - return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID) - } - - return nil -} - -// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner -func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { - - // TODO: mutex? - - var eakIDs []string - r, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) - if err != nil { - if !nosqlDB.IsErrNotFound(err) { - return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) - } - } else { - if err := json.Unmarshal(r, &eakIDs); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) - } - } - - keys := []*acme.ExternalAccountKey{} - for _, eakID := range eakIDs { - if eakID == "" { - continue // shouldn't happen; just in case - } - 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) - } - } - keys = append(keys, &acme.ExternalAccountKey{ - ID: eak.ID, - KeyBytes: eak.KeyBytes, - ProvisionerID: eak.ProvisionerID, - Reference: eak.Reference, - AccountID: eak.AccountID, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, - }) - } - - return keys, nil -} - -// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference -func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { - if reference == "" { - return nil, nil - } - - k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, reference))) - if nosqlDB.IsErrNotFound(err) { - return nil, acme.ErrNotFound - } else if err != nil { - return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) - } - dbExternalAccountKeyReference := new(dbExternalAccountKeyReference) - if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) - } - - return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) -} - -func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - old, err := db.getDBExternalAccountKey(ctx, eak.ID) - if err != nil { - return err - } - - if old.ProvisionerID != provisionerID { - return errors.New("provisioner does not match provisioner for which the EAB key was created") - } - - if old.ProvisionerID != eak.ProvisionerID { - return errors.New("cannot change provisioner for an existing ACME EAB Key") - } - - if old.Reference != eak.Reference { - return errors.New("cannot change reference for an existing ACME EAB Key") - } - - nu := dbExternalAccountKey{ - ID: eak.ID, - ProvisionerID: eak.ProvisionerID, - Reference: eak.Reference, - AccountID: eak.AccountID, - KeyBytes: eak.KeyBytes, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, - } - - return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) -} - -func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { - referencesByProvisionerIndexMux.Lock() - defer referencesByProvisionerIndexMux.Unlock() - - var eakIDs []string - b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) - if err != nil { - if !nosqlDB.IsErrNotFound(err) { - return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) - } - } else { - if err := json.Unmarshal(b, &eakIDs); err != nil { - return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) - } - } - - var newEAKIDs []string - newEAKIDs = append(newEAKIDs, eakIDs...) - newEAKIDs = append(newEAKIDs, eakID) - var ( - _old interface{} = eakIDs - _new interface{} = newEAKIDs - ) - - if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { - return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) - } - - return nil -} - -func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error { - referencesByProvisionerIndexMux.Lock() - defer referencesByProvisionerIndexMux.Unlock() - - var eakIDs []string - b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) - if err != nil { - if !nosqlDB.IsErrNotFound(err) { - return errors.Wrapf(err, "error loading reference IDs for provisioner %s", provisionerID) - } - } else { - if err := json.Unmarshal(b, &eakIDs); err != nil { - return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) - } - } - - newEAKIDs := removeElement(eakIDs, eakID) - var ( - _old interface{} = eakIDs - _new interface{} = newEAKIDs - ) - - if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { - return errors.Wrapf(err, "error saving referenceIDs index for provisioner %s", provisionerID) - } - - return nil -} - -// referenceKey returns a unique key for a reference per provisioner -func referenceKey(provisionerID, reference string) string { - return provisionerID + "." + reference -} - -// sliceIndex finds the index of item in slice -func sliceIndex(slice []string, item string) int { - for i := range slice { - if slice[i] == item { - return i - } - } - return -1 -} - -// removeElement deletes the item if it exists in the -// slice. It returns a new slice, keeping the old one intact. -func removeElement(slice []string, item string) []string { - - newSlice := make([]string, 0) - index := sliceIndex(slice, item) - if index < 0 { - newSlice = append(newSlice, slice...) - return newSlice - } - - newSlice = append(newSlice, slice[:index]...) - - return append(newSlice, slice[index+1:]...) -} diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 77937c2e..83a23476 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -3,7 +3,6 @@ package nosql import ( "context" "encoding/json" - "fmt" "testing" "time" @@ -697,1087 +696,3 @@ func TestDB_UpdateAccount(t *testing.T) { }) } } - -func TestDB_getDBExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - dbeak *dbExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - err: nil, - dbeak: dbeak, - } - }, - "fail/not-found": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return nil, nosqldb.ErrNotFound - }, - }, - err: acme.ErrNotFound, - } - }, - "fail/db.Get-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return nil, errors.New("force") - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - "fail/unmarshal-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return []byte("foo"), nil - }, - }, - err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, dbeak.ID, tc.dbeak.ID) - assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) - assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID) - assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) - assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) - assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) - assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) - } - }) - } -} - -func TestDB_GetExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - eak *acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - eak: &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - } - }, - "fail/db.Get-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return nil, errors.New("force") - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - "fail/non-matching-provisioner": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: "aDifferentProvID", - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - eak: &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, eak.ID, tc.eak.ID) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) - assert.Equals(t, eak.Reference, tc.eak.Reference) - assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, eak.AccountID, tc.eak.AccountID) - assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) - } - }) - } -} - -func TestDB_GetExternalAccountKeyByReference(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - ref string - acmeErr *acme.Error - eak *acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - }, - eak: &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - err: nil, - } - }, - "ok/no-reference": func(t *testing.T) test { - return test{ - ref: "", - eak: nil, - err: nil, - } - }, - "fail/reference-not-found": func(t *testing.T) test { - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), provID+"."+ref) - return nil, nosqldb.ErrNotFound - }, - }, - err: errors.New("not found"), - } - }, - "fail/reference-load-error": func(t *testing.T) test { - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), provID+"."+ref) - return nil, errors.New("force") - }, - }, - err: errors.New("error loading ACME EAB key for reference ref: force"), - } - }, - "fail/reference-unmarshal-error": func(t *testing.T) test { - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), provID+"."+ref) - return []byte{0}, nil - }, - }, - err: errors.New("error unmarshaling ACME EAB key for reference ref"), - } - }, - "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return nil, errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } - } else if assert.Nil(t, tc.err) && tc.eak != nil { - assert.Equals(t, eak.ID, tc.eak.ID) - assert.Equals(t, eak.AccountID, tc.eak.AccountID) - assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) - assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) - assert.Equals(t, eak.Reference, tc.eak.Reference) - } - }) - } -} - -func TestDB_GetExternalAccountKeys(t *testing.T) { - keyID1 := "keyID1" - keyID2 := "keyID2" - keyID3 := "keyID3" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - eaks []*acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak1 := &dbExternalAccountKey{ - ID: keyID1, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b1, err := json.Marshal(dbeak1) - assert.FatalError(t, err) - dbeak2 := &dbExternalAccountKey{ - ID: keyID2, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b2, err := json.Marshal(dbeak2) - assert.FatalError(t, err) - dbeak3 := &dbExternalAccountKey{ - ID: keyID3, - ProvisionerID: "aDifferentProvID", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b3, err := json.Marshal(dbeak3) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - keys := []string{keyID1, keyID2} - b, err := json.Marshal(keys) - assert.FatalError(t, err) - return b, nil - case string(externalAccountKeyTable): - switch string(key) { - case keyID1: - return b1, nil - case keyID2: - return b2, nil - default: - assert.FatalError(t, errors.Errorf("unexpected key %s", string(key))) - return nil, errors.New("force") - } - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - // TODO: remove the MList - MList: func(bucket []byte) ([]*nosqldb.Entry, error) { - switch string(bucket) { - case string(externalAccountKeyTable): - return []*nosqldb.Entry{ - { - Bucket: bucket, - Key: []byte(keyID1), - Value: b1, - }, - { - Bucket: bucket, - Key: []byte(keyID2), - Value: b2, - }, - { - Bucket: bucket, - Key: []byte(keyID3), - Value: b3, - }, - }, nil - case string(externalAccountKeysByProvisionerIDTable): - keys := []string{keyID1, keyID2} - b, err := json.Marshal(keys) - assert.FatalError(t, err) - return []*nosqldb.Entry{ - { - Bucket: bucket, - Key: []byte(provID), - Value: b, - }, - }, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force default") - } - }, - }, - eaks: []*acme.ExternalAccountKey{ - { - ID: keyID1, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - { - ID: keyID2, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - }, - } - }, - "fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) - return nil, errors.New("force") - }, - }, - err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"), - } - }, - "fail/db.getDBExternalAccountKey": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - keys := []string{keyID1, keyID2} - b, err := json.Marshal(keys) - assert.FatalError(t, err) - return b, nil - case string(externalAccountKeyTable): - return nil, errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force bucket") - } - }, - }, - err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"), - } - }, - } - for name, run := range tests { - 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 { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.Equals(t, tc.err.Error(), err.Error()) - } - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, len(eaks), len(tc.eaks)) - for i, eak := range eaks { - assert.Equals(t, eak.ID, tc.eaks[i].ID) - assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) - assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID) - assert.Equals(t, eak.Reference, tc.eaks[i].Reference) - assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) - assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) - assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) - } - } - }) - } -} - -func TestDB_DeleteExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - b, err := json.Marshal([]string{keyID}) - assert.FatalError(t, err) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force default") - } - }, - MDel: func(bucket, key []byte) error { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return errors.New("force default") - } - }, - MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) { - fmt.Println(string(bucket)) - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, provID+"."+ref, string(key)) - return nil, true, nil - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - return nil, true, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force default") - } - }, - }, - } - }, - "fail/not-found": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeyTable)) - assert.Equals(t, string(key), keyID) - return nil, nosqldb.ErrNotFound - }, - }, - err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"), - } - }, - "fail/non-matching-provisioner": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: "aDifferentProvID", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeyTable)) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - err: errors.New("provisioner does not match provisioner for which the EAB key was created"), - } - }, - "fail/delete-reference": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force default") - } - }, - MDel: func(bucket, key []byte) error { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return errors.New("force") - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return errors.New("force default") - } - }, - }, - err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"), - } - }, - "fail/delete-eak": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - MDel: func(bucket, key []byte) error { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return errors.New("force") - } - }, - }, - err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.Equals(t, err.Error(), tc.err.Error()) - } - } - } else { - assert.Nil(t, tc.err) - } - }) - } -} - -func TestDB_CreateExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - _id *string - eak *acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - var ( - id string - idPtr = &id - ) - now := clock.Now() - eak := &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - CreatedAt: now, - } - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) - assert.Equals(t, provID, string(key)) - b, _ := json.Marshal([]string{}) - return b, nil - }, - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - return nu, true, nil - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, provID+"."+ref, string(key)) - assert.Equals(t, nil, old) - return nu, true, nil - case string(externalAccountKeyTable): - assert.Equals(t, nil, old) - - id = string(key) - - dbeak := new(dbExternalAccountKey) - assert.FatalError(t, json.Unmarshal(nu, dbeak)) - assert.Equals(t, string(key), dbeak.ID) - assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID) - assert.Equals(t, eak.Reference, dbeak.Reference) - assert.Equals(t, 32, len(dbeak.KeyBytes)) - assert.False(t, dbeak.CreatedAt.IsZero()) - assert.Equals(t, dbeak.AccountID, eak.AccountID) - assert.True(t, dbeak.BoundAt.IsZero()) - return nu, true, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force default") - } - }, - }, - eak: eak, - _id: idPtr, - } - }, - "fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) - assert.Equals(t, old, nil) - return nu, true, nil - case string(externalAccountKeyTable): - assert.Equals(t, old, nil) - return nu, true, errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force") - } - }, - }, - err: errors.New("error saving acme external_account_key: force"), - } - }, - "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) - assert.Equals(t, provID, string(key)) - b, _ := json.Marshal([]string{}) - return b, nil - }, - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - return nu, true, nil - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, provID+"."+ref, string(key)) - assert.Equals(t, old, nil) - return nu, true, errors.New("force") - case string(externalAccountKeyTable): - assert.Equals(t, old, nil) - return nu, true, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force") - } - }, - }, - err: errors.New("error saving acme external_account_key_reference: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref) - fmt.Println(name, err) - if err != nil { - if assert.NotNil(t, tc.err) { - assert.Equals(t, err.Error(), tc.err.Error()) - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, *tc._id, eak.ID) - assert.Equals(t, provID, eak.ProvisionerID) - assert.Equals(t, ref, eak.Reference) - assert.Equals(t, "", eak.AccountID) - assert.False(t, eak.CreatedAt.IsZero()) - assert.False(t, eak.AlreadyBound()) - assert.True(t, eak.BoundAt.IsZero()) - } - }) - } -} - -func TestDB_UpdateExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - type test struct { - db nosql.DB - eak *acme.ExternalAccountKey - err error - } - var tests = map[string]func(t *testing.T) test{ - - "ok": func(t *testing.T) test { - eak := &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - return test{ - eak: eak, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return b, nil - }, - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, old, b) - - dbNew := new(dbExternalAccountKey) - assert.FatalError(t, json.Unmarshal(nu, dbNew)) - assert.Equals(t, dbNew.ID, dbeak.ID) - assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID) - assert.Equals(t, dbNew.Reference, dbeak.Reference) - assert.Equals(t, dbNew.AccountID, dbeak.AccountID) - assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) - assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) - assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) - return nu, true, nil - }, - }, - } - }, - "fail/provisioner-mismatch": func(t *testing.T) test { - newDBEAK := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: "aDifferentProvID", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(newDBEAK) - assert.FatalError(t, err) - return test{ - eak: &acme.ExternalAccountKey{ - ID: keyID, - }, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return b, nil - }, - }, - err: errors.New("provisioner does not match provisioner for which the EAB key was created"), - } - }, - "fail/db.Get-error": func(t *testing.T) test { - return test{ - eak: &acme.ExternalAccountKey{ - ID: keyID, - }, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return nil, errors.New("force") - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil { - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, dbeak.ID, tc.eak.ID) - assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID) - assert.Equals(t, dbeak.Reference, tc.eak.Reference) - assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) - assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) - assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) - } - }) - } -} diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go new file mode 100644 index 00000000..6b6334a5 --- /dev/null +++ b/acme/db/nosql/eab.go @@ -0,0 +1,306 @@ +package nosql + +import ( + "context" + "crypto/rand" + "encoding/json" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" + nosqlDB "github.com/smallstep/nosql" +) + +// CreateExternalAccountKey creates a new External Account Binding key with a name +func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + keyID, err := randID() + if err != nil { + return nil, err + } + + random := make([]byte, 32) + _, err = rand.Read(random) + if err != nil { + return nil, err + } + + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provisionerID, + Reference: reference, + KeyBytes: random, + CreatedAt: clock.Now(), + } + + if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { + return nil, err + } + + if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil { + return nil, err + } + + if dbeak.Reference != "" { + dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ + Reference: dbeak.Reference, + ExternalAccountKeyID: dbeak.ID, + } + if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { + return nil, err + } + } + + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} + +// GetExternalAccountKey retrieves an External Account Binding key by KeyID +func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) + if err != nil { + return nil, err + } + + if dbeak.ProvisionerID != provisionerID { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created") + } + + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} + +func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) + if err != nil { + return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) + } + + if dbeak.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") + } + + if dbeak.Reference != "" { + if err := db.db.Del(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference) + } + } + if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) + } + if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil { + return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID) + } + + return nil +} + +// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { + + // TODO: mutex? + + 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) + } + // 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) + } + } + + keys := []*acme.ExternalAccountKey{} + for _, eakID := range eakIDs { + if eakID == "" { + continue // shouldn't happen; just in case + } + 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) + } + } + keys = append(keys, &acme.ExternalAccountKey{ + ID: eak.ID, + KeyBytes: eak.KeyBytes, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + }) + } + + return keys, nil +} + +// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference +func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + if reference == "" { + return nil, nil + } + + k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, reference))) + if nosqlDB.IsErrNotFound(err) { + return nil, acme.ErrNotFound + } else if err != nil { + return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) + } + dbExternalAccountKeyReference := new(dbExternalAccountKeyReference) + if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) + } + + return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) +} + +func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + old, err := db.getDBExternalAccountKey(ctx, eak.ID) + if err != nil { + return err + } + + if old.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") + } + + if old.ProvisionerID != eak.ProvisionerID { + return errors.New("cannot change provisioner for an existing ACME EAB Key") + } + + if old.Reference != eak.Reference { + return errors.New("cannot change reference for an existing ACME EAB Key") + } + + nu := dbExternalAccountKey{ + ID: eak.ID, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + } + + return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) +} + +func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMux.Lock() + defer referencesByProvisionerIndexMux.Unlock() + + if eakID == "" { + return errors.Errorf("can't add empty eakID for provisioner %s", provisionerID) + } + + var eakIDs []string + b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + for _, id := range eakIDs { + if id == eakID { + // return an error when a duplicate ID is found + return errors.Errorf("eakID %s already exists for provisioner %s", eakID, provisionerID) + } + } + + var newEAKIDs []string + newEAKIDs = append(newEAKIDs, eakIDs...) + newEAKIDs = append(newEAKIDs, eakID) + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + + return nil +} + +func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMux.Lock() + defer referencesByProvisionerIndexMux.Unlock() + + var eakIDs []string + b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + newEAKIDs := removeElement(eakIDs, eakID) + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + + return nil +} + +// referenceKey returns a unique key for a reference per provisioner +func referenceKey(provisionerID, reference string) string { + return provisionerID + "." + reference +} + +// sliceIndex finds the index of item in slice +func sliceIndex(slice []string, item string) int { + for i := range slice { + if slice[i] == item { + return i + } + } + return -1 +} + +// removeElement deletes the item if it exists in the +// slice. It returns a new slice, keeping the old one intact. +func removeElement(slice []string, item string) []string { + + newSlice := make([]string, 0) + index := sliceIndex(slice, item) + if index < 0 { + newSlice = append(newSlice, slice...) + return newSlice + } + + newSlice = append(newSlice, slice[:index]...) + + return append(newSlice, slice[index+1:]...) +} diff --git a/acme/db/nosql/eab_test.go b/acme/db/nosql/eab_test.go new file mode 100644 index 00000000..6db0b734 --- /dev/null +++ b/acme/db/nosql/eab_test.go @@ -0,0 +1,1710 @@ +package nosql + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + certdb "github.com/smallstep/certificates/db" + "github.com/smallstep/nosql" + nosqldb "github.com/smallstep/nosql/database" +) + +func TestDB_getDBExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + dbeak *dbExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: nil, + dbeak: dbeak, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: acme.ErrNotFound, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return []byte("foo"), nil + }, + }, + err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.dbeak.ID) + assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) + assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID) + assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) + assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) + assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) + assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) + } + }) + } +} + +func TestDB_GetExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, eak.Reference, tc.eak.Reference) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + } + }) + } +} + +func TestDB_GetExternalAccountKeyByReference(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + ref string + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + err: nil, + } + }, + "ok/no-reference": func(t *testing.T) test { + return test{ + ref: "", + eak: nil, + err: nil, + } + }, + "fail/reference-not-found": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("not found"), + } + }, + "fail/reference-load-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB key for reference ref: force"), + } + }, + "fail/reference-unmarshal-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return []byte{0}, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB key for reference ref"), + } + }, + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) && tc.eak != nil { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, eak.Reference, tc.eak.Reference) + } + }) + } +} + +func TestDB_GetExternalAccountKeys(t *testing.T) { + keyID1 := "keyID1" + keyID2 := "keyID2" + keyID3 := "keyID3" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak1 := &dbExternalAccountKey{ + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b1, err := json.Marshal(dbeak1) + assert.FatalError(t, err) + dbeak2 := &dbExternalAccountKey{ + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b2, err := json.Marshal(dbeak2) + assert.FatalError(t, err) + dbeak3 := &dbExternalAccountKey{ + ID: keyID3, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b3, err := json.Marshal(dbeak3) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{"", keyID1, keyID2} // includes an empty keyID + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + switch string(key) { + case keyID1: + return b1, nil + case keyID2: + return b2, nil + default: + assert.FatalError(t, errors.Errorf("unexpected key %s", string(key))) + return nil, errors.New("force default") + } + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + // TODO: remove the MList + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + switch string(bucket) { + case string(externalAccountKeyTable): + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: b1, + }, + { + Bucket: bucket, + Key: []byte(keyID2), + Value: b2, + }, + { + Bucket: bucket, + Key: []byte(keyID3), + Value: b3, + }, + }, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(provID), + Value: b, + }, + }, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + }, + eaks: []*acme.ExternalAccountKey{ + { + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + { + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + }, + } + }, + "fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"), + } + }, + "fail/db.Get-externalAccountKeysByProvisionerIDTable-unmarshal": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB Key IDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/db.getDBExternalAccountKey": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force bucket") + } + }, + }, + err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"), + } + }, + } + for name, run := range tests { + 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 { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.Equals(t, tc.err.Error(), err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, len(eaks), len(tc.eaks)) + for i, eak := range eaks { + assert.Equals(t, eak.ID, tc.eaks[i].ID) + assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID) + assert.Equals(t, eak.Reference, tc.eaks[i].Reference) + assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) + assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) + assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) + } + } + }) + } +} + +func TestDB_DeleteExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + b, err := json.Marshal([]string{keyID}) + assert.FatalError(t, err) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) { + fmt.Println(string(bucket)) + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + return nil, true, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nil, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/delete-reference": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"), + } + }, + "fail/delete-eak": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"), + } + }, + "fail/delete-eakID": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + return b, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error removing ACME EAB Key ID keyID: error loading eakIDs for provisioner provID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.Equals(t, err.Error(), tc.err.Error()) + } + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestDB_CreateExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + _id *string + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + var ( + id string + idPtr = &id + ) + now := clock.Now() + eak := &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + CreatedAt: now, + } + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + assert.Equals(t, nil, old) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, nil, old) + + id = string(key) + + dbeak := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbeak)) + assert.Equals(t, string(key), dbeak.ID) + assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID) + assert.Equals(t, eak.Reference, dbeak.Reference) + assert.Equals(t, 32, len(dbeak.KeyBytes)) + assert.False(t, dbeak.CreatedAt.IsZero()) + assert.Equals(t, dbeak.AccountID, eak.AccountID) + assert.True(t, dbeak.BoundAt.IsZero()) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + eak: eak, + _id: idPtr, + } + }, + "fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error saving acme external_account_key: force"), + } + }, + "fail/addEAKID-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + return nil, errors.New("force") + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error saving acme external_account_key_reference: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.Equals(t, err.Error(), tc.err.Error()) + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, *tc._id, eak.ID) + assert.Equals(t, provID, eak.ProvisionerID) + assert.Equals(t, ref, eak.Reference) + assert.Equals(t, "", eak.AccountID) + assert.False(t, eak.CreatedAt.IsZero()) + assert.False(t, eak.AlreadyBound()) + assert.True(t, eak.BoundAt.IsZero()) + } + }) + } +} + +func TestDB_UpdateExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + type test struct { + db nosql.DB + eak *acme.ExternalAccountKey + err error + } + var tests = map[string]func(t *testing.T) test{ + + "ok": func(t *testing.T) test { + eak := &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + return test{ + eak: eak, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, old, b) + + dbNew := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbNew)) + assert.Equals(t, dbNew.ID, dbeak.ID) + assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID) + assert.Equals(t, dbNew.Reference, dbeak.Reference) + assert.Equals(t, dbNew.AccountID, dbeak.AccountID) + assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) + assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) + assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) + return nu, true, nil + }, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/provisioner-mismatch": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + }, + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/provisioner-change": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvisionerID", + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("cannot change provisioner for an existing ACME EAB Key"), + } + }, + "fail/reference-change": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "aDifferentReference", + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("cannot change reference for an existing ACME EAB Key"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.eak.ID) + assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, dbeak.Reference, tc.eak.Reference) + assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) + assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) + } + }) + } +} + +func TestDB_addEAKID(t *testing.T) { + provID := "provID" + eakID := "eakID" + type test struct { + ctx context.Context + provisionerID string + eakID string + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "fail/empty-eakID": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: "", + err: errors.New("can't add empty eakID for provisioner provID"), + } + }, + "fail/db.Get": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/unmarshal": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/eakID-already-exists": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{eakID}) + return b, nil + }, + }, + err: errors.New("eakID eakID already exists for provisioner provID"), + } + }, + "fail/db.save": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", eakID}) + assert.Equals(t, nu, newB) + return newB, true, errors.New("force") + }, + }, + err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"), + } + }, + "ok/db.Get-not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, nosqldb.ErrNotFound + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + assert.Equals(t, old, []byte{110, 117, 108, 108}) + b, _ := json.Marshal([]string{eakID}) + assert.Equals(t, nu, b) + return b, true, nil + }, + }, + err: nil, + } + }, + "ok": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", "id2"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", "id2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", "id2", eakID}) + assert.Equals(t, nu, newB) + return newB, true, nil + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := &DB{ + db: tc.db, + } + wantErr := tc.err != nil + err := db.addEAKID(tc.ctx, tc.provisionerID, tc.eakID) + if (err != nil) != wantErr { + t.Errorf("DB.addEAKID() error = %v, wantErr %v", err, wantErr) + } + if err != nil { + assert.Equals(t, tc.err.Error(), err.Error()) + } + }) + } +} + +func TestDB_deleteEAKID(t *testing.T) { + provID := "provID" + eakID := "eakID" + type test struct { + ctx context.Context + provisionerID string + eakID string + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "fail/db.Get": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/unmarshal": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/db.save": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", eakID}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", eakID}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1"}) + assert.Equals(t, nu, newB) + return newB, true, errors.New("force") + }, + }, + err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"), + } + }, + "ok/db.Get-not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, nosqldb.ErrNotFound + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + assert.Equals(t, old, []byte{110, 117, 108, 108}) + b, _ := json.Marshal([]string{}) + assert.Equals(t, nu, b) + return b, true, nil + }, + }, + err: nil, + } + }, + "ok": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", eakID, "id2"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", eakID, "id2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", "id2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := &DB{ + db: tc.db, + } + wantErr := tc.err != nil + err := db.deleteEAKID(tc.ctx, tc.provisionerID, tc.eakID) + if (err != nil) != wantErr { + t.Errorf("DB.deleteEAKID() error = %v, wantErr %v", err, wantErr) + } + if err != nil { + assert.Equals(t, tc.err.Error(), err.Error()) + } + }) + } +} + +func TestDB_addAndDeleteEAKID(t *testing.T) { + provID := "provID" + callCounter := 0 + type test struct { + ctx context.Context + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "ok/multi": func(t *testing.T) test { + return test{ + ctx: context.Background(), + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + switch callCounter { + case 0: + return nil, nosqldb.ErrNotFound + case 1: + b, _ := json.Marshal([]string{"eakID"}) + return b, nil + case 2: + b, _ := json.Marshal([]string{}) + return b, nil + case 3: + b, _ := json.Marshal([]string{"eakID1"}) + return b, nil + case 4: + b, _ := json.Marshal([]string{"eakID1", "eakID2"}) + return b, nil + case 5: + b, _ := json.Marshal([]string{"eakID2"}) + return b, nil + default: + assert.FatalError(t, errors.New("unexpected get iteration")) + return nil, errors.New("force get default") + } + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + switch callCounter { + case 0: + assert.Equals(t, old, []byte{110, 117, 108, 108}) + newB, _ := json.Marshal([]string{"eakID"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 1: + oldB, _ := json.Marshal([]string{"eakID"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{}) + return newB, true, nil + case 2: + oldB, _ := json.Marshal([]string{}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID1"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 3: + oldB, _ := json.Marshal([]string{"eakID1"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID1", "eakID2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 4: + oldB, _ := json.Marshal([]string{"eakID1", "eakID2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 5: + oldB, _ := json.Marshal([]string{"eakID2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{}) + assert.Equals(t, nu, newB) + return newB, true, nil + default: + assert.FatalError(t, errors.New("unexpected get iteration")) + return nil, true, errors.New("force save default") + } + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + + // goal of this test is to simulate multiple calls; no errors expected. + + db := &DB{ + db: tc.db, + } + + err := db.addEAKID(tc.ctx, provID, "eakID") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID") + if err != nil { + t.Errorf("DB.deleteEAKID() error = %v", err) + } + + callCounter++ + err = db.addEAKID(tc.ctx, provID, "eakID1") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.addEAKID(tc.ctx, provID, "eakID2") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID1") + if err != nil { + t.Errorf("DB.deleteEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID2") + if err != nil { + t.Errorf("DB.deleteAKID() error = %v", err) + } + }) + } +} + +func Test_removeElement(t *testing.T) { + tests := []struct { + name string + slice []string + item string + want []string + }{ + { + name: "remove-first", + slice: []string{"id1", "id2", "id3"}, + item: "id1", + want: []string{"id2", "id3"}, + }, + { + name: "remove-last", + slice: []string{"id1", "id2", "id3"}, + item: "id3", + want: []string{"id1", "id2"}, + }, + { + name: "remove-middle", + slice: []string{"id1", "id2", "id3"}, + item: "id2", + want: []string{"id1", "id3"}, + }, + { + name: "remove-non-existing", + slice: []string{"id1", "id2", "id3"}, + item: "none", + want: []string{"id1", "id2", "id3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := removeElement(tt.slice, tt.item) + if !cmp.Equal(tt.want, got) { + t.Errorf("removeElement() diff =\n %s", cmp.Diff(tt.want, got)) + } + }) + } +} diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 8343c196..2de82b70 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -11,18 +11,18 @@ import ( ) var ( - accountTable = []byte("acme_accounts") - accountByKeyIDTable = []byte("acme_keyID_accountID_index") - authzTable = []byte("acme_authzs") - challengeTable = []byte("acme_challenges") - nonceTable = []byte("nonces") - orderTable = []byte("acme_orders") - ordersByAccountIDTable = []byte("acme_account_orders_index") - certTable = []byte("acme_certs") - certBySerialTable = []byte("acme_serial_certs_index") - externalAccountKeyTable = []byte("acme_external_account_keys") - externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") - externalAccountKeysByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") + accountTable = []byte("acme_accounts") + accountByKeyIDTable = []byte("acme_keyID_accountID_index") + authzTable = []byte("acme_authzs") + challengeTable = []byte("acme_challenges") + nonceTable = []byte("nonces") + orderTable = []byte("acme_orders") + ordersByAccountIDTable = []byte("acme_account_orders_index") + certTable = []byte("acme_certs") + certBySerialTable = []byte("acme_serial_certs_index") + externalAccountKeyTable = []byte("acme_external_account_keys") + externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") + externalAccountKeyIDsByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") ) // DB is a struct that implements the AcmeDB interface. @@ -35,7 +35,7 @@ func New(db nosqlDB.DB) (*DB, error) { tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, certBySerialTable, externalAccountKeyTable, - externalAccountKeysByReferenceTable, externalAccountKeysByProvisionerIDTable, + externalAccountKeysByReferenceTable, externalAccountKeyIDsByProvisionerIDTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index f7de9290..475e6606 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -391,6 +391,54 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } } +func Test_provisionerFromContext(t *testing.T) { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "acmeProv", + } + tests := []struct { + name string + ctx context.Context + want *linkedca.Provisioner + wantErr bool + }{ + { + name: "fail/no-provisioner", + ctx: context.Background(), + want: nil, + wantErr: true, + }, + { + name: "fail/wrong-type", + ctx: context.WithValue(context.Background(), provisionerContextKey, "prov"), + want: nil, + wantErr: true, + }, + { + name: "ok", + ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + want: &linkedca.Provisioner{ + Id: "provID", + Name: "acmeProv", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := provisionerFromContext(tt.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("provisionerFromContext() error = %v, wantErr %v", err, tt.wantErr) + return + } + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{})} + if !cmp.Equal(tt.want, got, opts...) { + t.Errorf("provisionerFromContext() diff =\n %s", cmp.Diff(tt.want, got, opts...)) + } + }) + } +} + func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { type fields struct { Reference string @@ -488,6 +536,28 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, } }, + "fail/no-provisioner-in-context": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "aRef", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: ctx, + body: body, + statusCode: 500, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error getting provisioner from context: provisioner expected in request context", + }, + } + }, "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") @@ -759,6 +829,21 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { err *admin.Error } 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") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + return test{ + ctx: ctx, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error getting provisioner from context: provisioner expected in request context", + }, + } + }, "fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") @@ -861,6 +946,23 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { err *admin.Error } 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") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := httptest.NewRequest("GET", "/foo", nil) + return test{ + ctx: ctx, + statusCode: 500, + req: req, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error getting provisioner from context: provisioner expected in request context", + }, + } + }, "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") From c3f2fd8ef0a2691fa706ddfcbc2e4440e0cbdc8a Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 20 Jan 2022 17:24:35 +0100 Subject: [PATCH 45/47] Add RW locks to prevent concurrent updates to the DB Although this may slow certain API calls down and may not be, strictly necessary, I think it's best to put all the ACME EAB operations behind RW locks to prevent concurrent updates to the DB and guarantee consistent result sets. --- acme/db/nosql/account.go | 37 --------------------- acme/db/nosql/eab.go | 69 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 603e3f2d..1c3bec5d 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -3,7 +3,6 @@ package nosql import ( "context" "encoding/json" - "sync" "time" "github.com/pkg/errors" @@ -12,9 +11,6 @@ import ( "go.step.sm/crypto/jose" ) -// Mutex for locking referencesByProvisioner index operations. -var referencesByProvisionerIndexMux sync.Mutex - // dbAccount represents an ACME account. type dbAccount struct { ID string `json:"id"` @@ -30,21 +26,6 @@ func (dba *dbAccount) clone() *dbAccount { return &nu } -type dbExternalAccountKey struct { - ID string `json:"id"` - ProvisionerID string `json:"provisionerID"` - Reference string `json:"reference"` - AccountID string `json:"accountID,omitempty"` - KeyBytes []byte `json:"key"` - CreatedAt time.Time `json:"createdAt"` - BoundAt time.Time `json:"boundAt"` -} - -type dbExternalAccountKeyReference struct { - Reference string `json:"reference"` - ExternalAccountKeyID string `json:"externalAccountKeyID"` -} - func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) { id, err := db.db.Get(accountByKeyIDTable, []byte(kid)) if err != nil { @@ -73,24 +54,6 @@ func (db *DB) getDBAccount(ctx context.Context, id string) (*dbAccount, error) { return dbacc, nil } -// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey. -func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) { - data, err := db.db.Get(externalAccountKeyTable, []byte(id)) - if err != nil { - if nosqlDB.IsErrNotFound(err) { - return nil, acme.ErrNotFound - } - return nil, errors.Wrapf(err, "error loading external account key %s", id) - } - - dbeak := new(dbExternalAccountKey) - if err = json.Unmarshal(data, dbeak); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", id) - } - - return dbeak, nil -} - // GetAccount retrieves an ACME account by ID. func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) { dbacc, err := db.getDBAccount(ctx, id) diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go index 6b6334a5..51f0e053 100644 --- a/acme/db/nosql/eab.go +++ b/acme/db/nosql/eab.go @@ -4,14 +4,59 @@ import ( "context" "crypto/rand" "encoding/json" + "sync" + "time" "github.com/pkg/errors" "github.com/smallstep/certificates/acme" nosqlDB "github.com/smallstep/nosql" ) +// externalAccountKeyMutex for read/write locking of EAK operations. +var externalAccountKeyMutex sync.RWMutex + +// referencesByProvisionerIndexMutex for locking referencesByProvisioner index operations. +var referencesByProvisionerIndexMutex sync.Mutex + +type dbExternalAccountKey struct { + ID string `json:"id"` + ProvisionerID string `json:"provisionerID"` + Reference string `json:"reference"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` +} + +type dbExternalAccountKeyReference struct { + Reference string `json:"reference"` + ExternalAccountKeyID string `json:"externalAccountKeyID"` +} + +// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey. +func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) { + data, err := db.db.Get(externalAccountKeyTable, []byte(id)) + if err != nil { + if nosqlDB.IsErrNotFound(err) { + return nil, acme.ErrNotFound + } + return nil, errors.Wrapf(err, "error loading external account key %s", id) + } + + dbeak := new(dbExternalAccountKey) + if err = json.Unmarshal(data, dbeak); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", id) + } + + return dbeak, nil +} + // CreateExternalAccountKey creates a new External Account Binding key with a name func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + + externalAccountKeyMutex.Lock() + defer externalAccountKeyMutex.Unlock() + keyID, err := randID() if err != nil { return nil, err @@ -62,6 +107,9 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, refer // GetExternalAccountKey retrieves an External Account Binding key by KeyID func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { + externalAccountKeyMutex.RLock() + defer externalAccountKeyMutex.RUnlock() + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return nil, err @@ -83,6 +131,9 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID st } func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { + externalAccountKeyMutex.Lock() + defer externalAccountKeyMutex.Unlock() + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) if err != nil { return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) @@ -109,8 +160,8 @@ 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) { - - // TODO: mutex? + externalAccountKeyMutex.RLock() + defer externalAccountKeyMutex.RUnlock() var eakIDs []string r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) @@ -152,6 +203,9 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + externalAccountKeyMutex.RLock() + defer externalAccountKeyMutex.RUnlock() + if reference == "" { return nil, nil } @@ -171,6 +225,9 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI } func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + externalAccountKeyMutex.Lock() + defer externalAccountKeyMutex.Unlock() + old, err := db.getDBExternalAccountKey(ctx, eak.ID) if err != nil { return err @@ -202,8 +259,8 @@ func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string } func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { - referencesByProvisionerIndexMux.Lock() - defer referencesByProvisionerIndexMux.Unlock() + referencesByProvisionerIndexMutex.Lock() + defer referencesByProvisionerIndexMutex.Unlock() if eakID == "" { return errors.Errorf("can't add empty eakID for provisioner %s", provisionerID) @@ -245,8 +302,8 @@ func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { } func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error { - referencesByProvisionerIndexMux.Lock() - defer referencesByProvisionerIndexMux.Unlock() + referencesByProvisionerIndexMutex.Lock() + defer referencesByProvisionerIndexMutex.Unlock() var eakIDs []string b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) From fd9845e9c7b809fdad42ac131f8201ca5fddc000 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 24 Jan 2022 14:03:56 +0100 Subject: [PATCH 46/47] 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() From bf21319e76bf65b946ce5723de19f10059ee7f86 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 28 Jan 2022 13:26:56 +0100 Subject: [PATCH 47/47] Fix PR comments and issue with empty string slices --- acme/db/nosql/eab.go | 21 +++++++++++++++--- acme/db/nosql/eab_test.go | 45 +++++++++++++++++++-------------------- acme/db/nosql/nosql.go | 4 ++-- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go index 170f457d..f9a24daf 100644 --- a/acme/db/nosql/eab.go +++ b/acme/db/nosql/eab.go @@ -89,7 +89,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, refer Reference: dbeak.Reference, ExternalAccountKeyID: dbeak.ID, } - if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { + if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeyIDsByReferenceTable); err != nil { return nil, err } } @@ -144,7 +144,7 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID } if dbeak.Reference != "" { - if err := db.db.Del(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { + if err := db.db.Del(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference) } } @@ -212,7 +212,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI return nil, nil } - k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, reference))) + k, err := db.db.Get(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, reference))) if nosqlDB.IsErrNotFound(err) { return nil, acme.ErrNotFound } else if err != nil { @@ -291,11 +291,19 @@ func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { var newEAKIDs []string newEAKIDs = append(newEAKIDs, eakIDs...) newEAKIDs = append(newEAKIDs, eakID) + var ( _old interface{} = eakIDs _new interface{} = newEAKIDs ) + // ensure that the DB gets the expected value when the slice is empty; otherwise + // it'll return with an error that indicates that the DBs view of the data is + // different from the last read (i.e. _old is different from what the DB has). + if len(eakIDs) == 0 { + _old = nil + } + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) } @@ -326,6 +334,13 @@ func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) erro _new interface{} = newEAKIDs ) + // ensure that the DB gets the expected value when the slice is empty; otherwise + // it'll return with an error that indicates that the DBs view of the data is + // different from the last read (i.e. _old is different from what the DB has). + if len(eakIDs) == 0 { + _old = nil + } + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) } diff --git a/acme/db/nosql/eab_test.go b/acme/db/nosql/eab_test.go index be14d90b..568500e9 100644 --- a/acme/db/nosql/eab_test.go +++ b/acme/db/nosql/eab_test.go @@ -271,7 +271,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), provID+"."+ref) return dbrefBytes, nil case string(externalAccountKeyTable): @@ -306,7 +306,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { ref: ref, db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable)) assert.Equals(t, string(key), provID+"."+ref) return nil, nosqldb.ErrNotFound }, @@ -319,7 +319,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { ref: ref, db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable)) assert.Equals(t, string(key), provID+"."+ref) return nil, errors.New("force") }, @@ -332,7 +332,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { ref: ref, db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable)) assert.Equals(t, string(key), provID+"."+ref) return []byte{0}, nil }, @@ -352,7 +352,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) { db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), provID+"."+ref) return dbrefBytes, nil case string(externalAccountKeyTable): @@ -642,7 +642,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), provID+"."+ref) return dbrefBytes, nil case string(externalAccountKeyTable): @@ -660,7 +660,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { }, MDel: func(bucket, key []byte) error { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), provID+"."+ref) return nil case string(externalAccountKeyTable): @@ -674,7 +674,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) { fmt.Println(string(bucket)) switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, provID+"."+ref, string(key)) return nil, true, nil case string(externalAccountKeyIDsByProvisionerIDTable): @@ -745,7 +745,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), ref) return dbrefBytes, nil case string(externalAccountKeyTable): @@ -758,7 +758,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { }, MDel: func(bucket, key []byte) error { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), provID+"."+ref) return errors.New("force") case string(externalAccountKeyTable): @@ -795,7 +795,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), ref) return dbrefBytes, nil case string(externalAccountKeyTable): @@ -808,7 +808,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { }, MDel: func(bucket, key []byte) error { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), provID+"."+ref) return nil case string(externalAccountKeyTable): @@ -845,7 +845,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { db: &certdb.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), ref) return dbrefBytes, nil case string(externalAccountKeyTable): @@ -860,7 +860,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) { }, MDel: func(bucket, key []byte) error { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), provID+"."+ref) return nil case string(externalAccountKeyTable): @@ -939,7 +939,7 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { case string(externalAccountKeyIDsByProvisionerIDTable): assert.Equals(t, provID, string(key)) return nu, true, nil - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, provID+"."+ref, string(key)) assert.Equals(t, nil, old) return nu, true, nil @@ -973,7 +973,7 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { db: &certdb.MockNoSQLDB{ MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), ref) assert.Equals(t, old, nil) return nu, true, nil @@ -999,7 +999,7 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { }, MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { switch string(bucket) { - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, string(key), ref) assert.Equals(t, old, nil) return nu, true, nil @@ -1029,7 +1029,7 @@ func TestDB_CreateExternalAccountKey(t *testing.T) { case string(externalAccountKeyIDsByProvisionerIDTable): assert.Equals(t, provID, string(key)) return nu, true, nil - case string(externalAccountKeysByReferenceTable): + case string(externalAccountKeyIDsByReferenceTable): assert.Equals(t, provID+"."+ref, string(key)) assert.Equals(t, old, nil) return nu, true, errors.New("force") @@ -1348,7 +1348,7 @@ func TestDB_addEAKID(t *testing.T) { MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) assert.Equals(t, string(key), provID) - assert.Equals(t, old, []byte{110, 117, 108, 108}) + assert.Equals(t, old, nil) b, _ := json.Marshal([]string{eakID}) assert.Equals(t, nu, b) return b, true, nil @@ -1482,7 +1482,7 @@ func TestDB_deleteEAKID(t *testing.T) { MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) assert.Equals(t, string(key), provID) - assert.Equals(t, old, []byte{110, 117, 108, 108}) + assert.Equals(t, old, nil) b, _ := json.Marshal([]string{}) assert.Equals(t, nu, b) return b, true, nil @@ -1579,7 +1579,7 @@ func TestDB_addAndDeleteEAKID(t *testing.T) { assert.Equals(t, string(key), provID) switch callCounter { case 0: - assert.Equals(t, old, []byte{110, 117, 108, 108}) + assert.Equals(t, old, nil) newB, _ := json.Marshal([]string{"eakID"}) assert.Equals(t, nu, newB) return newB, true, nil @@ -1589,8 +1589,7 @@ func TestDB_addAndDeleteEAKID(t *testing.T) { newB, _ := json.Marshal([]string{}) return newB, true, nil case 2: - oldB, _ := json.Marshal([]string{}) - assert.Equals(t, old, oldB) + assert.Equals(t, old, nil) newB, _ := json.Marshal([]string{"eakID1"}) assert.Equals(t, nu, newB) return newB, true, nil diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 2de82b70..98f6a04d 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -21,7 +21,7 @@ var ( certTable = []byte("acme_certs") certBySerialTable = []byte("acme_serial_certs_index") externalAccountKeyTable = []byte("acme_external_account_keys") - externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") + externalAccountKeyIDsByReferenceTable = []byte("acme_external_account_keyID_reference_index") externalAccountKeyIDsByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") ) @@ -35,7 +35,7 @@ func New(db nosqlDB.DB) (*DB, error) { tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, certBySerialTable, externalAccountKeyTable, - externalAccountKeysByReferenceTable, externalAccountKeyIDsByProvisionerIDTable, + externalAccountKeyIDsByReferenceTable, externalAccountKeyIDsByProvisionerIDTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil {