forked from TrueCloudLab/certificates
Add External Accounting Binding key "BoundAt" marking
This commit is contained in:
parent
f81d49d963
commit
d44cd18b96
5 changed files with 90 additions and 33 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
11
acme/db.go
11
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue