From f11c0cdc0c6f515dc35ba5f60baa47faae04c8b7 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 27 Aug 2021 16:58:04 +0200 Subject: [PATCH] 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 }