Add External Accounting Binding key "BoundAt" marking

This commit is contained in:
Herman Slatman 2021-07-17 19:02:47 +02:00
parent f81d49d963
commit d44cd18b96
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
5 changed files with 90 additions and 33 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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,

View file

@ -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 {

View file

@ -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)
}