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
|
// Account is a subset of the internal account type containing only those
|
||||||
// attributes required for responses in the ACME protocol.
|
// attributes required for responses in the ACME protocol.
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID string `json:"-"`
|
ID string `json:"-"`
|
||||||
Key *jose.JSONWebKey `json:"-"`
|
Key *jose.JSONWebKey `json:"-"`
|
||||||
Contact []string `json:"contact,omitempty"`
|
Contact []string `json:"contact,omitempty"`
|
||||||
Status Status `json:"status"`
|
Status Status `json:"status"`
|
||||||
OrdersURL string `json:"orders"`
|
OrdersURL string `json:"orders"`
|
||||||
|
ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLog enables response logging.
|
// ToLog enables response logging.
|
||||||
|
@ -50,3 +51,9 @@ type ExternalAccountKey struct {
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
BoundAt time.Time `json:"boundAt,omitempty"`
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.validateExternalAccountBinding(ctx, &nar); err != nil {
|
eak, err := h.validateExternalAccountBinding(ctx, &nar)
|
||||||
|
if err != nil {
|
||||||
api.WriteError(w, err)
|
api.WriteError(w, err)
|
||||||
return
|
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
|
httpStatus := http.StatusCreated
|
||||||
acc, err := accountFromContext(r.Context())
|
acc, err := accountFromContext(r.Context())
|
||||||
if err != nil {
|
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"))
|
api.WriteError(w, acme.WrapErrorISE(err, "error creating account"))
|
||||||
return
|
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 {
|
} else {
|
||||||
// Account exists //
|
// Account exists //
|
||||||
httpStatus = http.StatusOK
|
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
|
// 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)
|
prov, err := provisionerFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner
|
acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner
|
||||||
if !ok || acmeProv == nil {
|
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
|
shouldSkipAccountBindingValidation := !acmeProv.RequireEAB
|
||||||
if shouldSkipAccountBindingValidation {
|
if shouldSkipAccountBindingValidation {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if nar.ExternalAccountBinding == 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)
|
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return acme.WrapErrorISE(err, "error marshalling externalAccountBinding")
|
return nil, acme.WrapErrorISE(err, "error marshalling externalAccountBinding")
|
||||||
}
|
}
|
||||||
|
|
||||||
eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes))
|
eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes))
|
||||||
if err != nil {
|
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)?
|
// 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
|
keyID := eabJWS.Signatures[0].Protected.KeyID
|
||||||
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, keyID)
|
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, keyID)
|
||||||
if err != nil {
|
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)
|
payload, err := eabJWS.Verify(externalAccountKey.KeyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
|
return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
jwk, err := jwkFromContext(ctx)
|
jwk, err := jwkFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
jwkJSONBytes, err := jwk.MarshalJSON()
|
jwkJSONBytes, err := jwk.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return acme.WrapErrorISE(err, "error marshaling jwk")
|
return nil, acme.WrapErrorISE(err, "error marshaling jwk")
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(payload, jwkJSONBytes) {
|
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)
|
b, err := json.Marshal(nar)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})
|
||||||
|
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||||
return test{
|
return test{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
statusCode: 400,
|
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)
|
CreateExternalAccountKey(ctx context.Context, name string) (*ExternalAccountKey, error)
|
||||||
GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error)
|
GetExternalAccountKey(ctx context.Context, keyID string) (*ExternalAccountKey, error)
|
||||||
|
UpdateExternalAccountKey(ctx context.Context, eak *ExternalAccountKey) error
|
||||||
|
|
||||||
CreateNonce(ctx context.Context) (Nonce, error)
|
CreateNonce(ctx context.Context) (Nonce, error)
|
||||||
DeleteNonce(ctx context.Context, nonce 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)
|
MockCreateExternalAccountKey func(ctx context.Context, name string) (*ExternalAccountKey, error)
|
||||||
MockGetExternalAccountKey func(ctx context.Context, keyID 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)
|
MockCreateNonce func(ctx context.Context) (Nonce, error)
|
||||||
MockDeleteNonce func(ctx context.Context, nonce 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
|
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
|
// CreateNonce mock
|
||||||
func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) {
|
func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) {
|
||||||
if m.MockCreateNonce != nil {
|
if m.MockCreateNonce != nil {
|
||||||
|
|
|
@ -31,7 +31,7 @@ type dbExternalAccountKey struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AccountID string `json:"accountID,omitempty"`
|
AccountID string `json:"accountID,omitempty"`
|
||||||
KeyBytes []byte `json:"key,omitempty"`
|
KeyBytes []byte `json:"key"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
BoundAt time.Time `json:"boundAt"`
|
BoundAt time.Time `json:"boundAt"`
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,24 @@ func (db *DB) getDBAccount(ctx context.Context, id string) (*dbAccount, error) {
|
||||||
return dbacc, nil
|
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.
|
// GetAccount retrieves an ACME account by ID.
|
||||||
func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) {
|
func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) {
|
||||||
dbacc, err := db.getDBAccount(ctx, id)
|
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
|
// GetExternalAccountKey retrieves an External Account Binding key by KeyID
|
||||||
func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.ExternalAccountKey, error) {
|
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 err != nil {
|
||||||
if nosqlDB.IsErrNotFound(err) {
|
return nil, 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 &acme.ExternalAccountKey{
|
return &acme.ExternalAccountKey{
|
||||||
|
@ -202,3 +212,21 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, keyID string) (*acme.Ex
|
||||||
BoundAt: dbeak.BoundAt,
|
BoundAt: dbeak.BoundAt,
|
||||||
}, nil
|
}, 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