From d44cd18b962fb3a9ab4bcca72852c945eae7ecbe Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 17 Jul 2021 19:02:47 +0200 Subject: [PATCH] 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) +}