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 }