diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 4cff6653..603e3f2d 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -2,7 +2,6 @@ package nosql import ( "context" - "crypto/rand" "encoding/json" "sync" "time" @@ -172,284 +171,3 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { return db.save(ctx, old.ID, nu, old, "account", accountTable) } - -// CreateExternalAccountKey creates a new External Account Binding key with a name -func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { - keyID, err := randID() - if err != nil { - return nil, err - } - - random := make([]byte, 32) - _, err = rand.Read(random) - if err != nil { - return nil, err - } - - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provisionerID, - Reference: reference, - KeyBytes: random, - CreatedAt: clock.Now(), - } - - if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { - return nil, err - } - - if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil { - return nil, err - } - - if dbeak.Reference != "" { - dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ - Reference: dbeak.Reference, - ExternalAccountKeyID: dbeak.ID, - } - if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { - return nil, err - } - } - - return &acme.ExternalAccountKey{ - ID: dbeak.ID, - ProvisionerID: dbeak.ProvisionerID, - Reference: dbeak.Reference, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, - }, nil -} - -// GetExternalAccountKey retrieves an External Account Binding key by KeyID -func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { - dbeak, err := db.getDBExternalAccountKey(ctx, keyID) - if err != nil { - return nil, err - } - - if dbeak.ProvisionerID != provisionerID { - return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created") - } - - return &acme.ExternalAccountKey{ - ID: dbeak.ID, - ProvisionerID: dbeak.ProvisionerID, - Reference: dbeak.Reference, - AccountID: dbeak.AccountID, - KeyBytes: dbeak.KeyBytes, - CreatedAt: dbeak.CreatedAt, - BoundAt: dbeak.BoundAt, - }, nil -} - -func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { - dbeak, err := db.getDBExternalAccountKey(ctx, keyID) - if err != nil { - return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) - } - - if dbeak.ProvisionerID != provisionerID { - return errors.New("provisioner does not match provisioner for which the EAB key was created") - } - - if dbeak.Reference != "" { - if err := db.db.Del(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { - return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference) - } - } - if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { - return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) - } - if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil { - return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID) - } - - return nil -} - -// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner -func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { - - // TODO: mutex? - - var eakIDs []string - r, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) - if err != nil { - if !nosqlDB.IsErrNotFound(err) { - return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) - } - } else { - if err := json.Unmarshal(r, &eakIDs); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) - } - } - - keys := []*acme.ExternalAccountKey{} - for _, eakID := range eakIDs { - if eakID == "" { - continue // shouldn't happen; just in case - } - eak, err := db.getDBExternalAccountKey(ctx, eakID) - if err != nil { - if !nosqlDB.IsErrNotFound(err) { - return nil, errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID) - } - } - keys = append(keys, &acme.ExternalAccountKey{ - ID: eak.ID, - KeyBytes: eak.KeyBytes, - ProvisionerID: eak.ProvisionerID, - Reference: eak.Reference, - AccountID: eak.AccountID, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, - }) - } - - return keys, nil -} - -// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference -func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { - if reference == "" { - return nil, nil - } - - k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, reference))) - if nosqlDB.IsErrNotFound(err) { - return nil, acme.ErrNotFound - } else if err != nil { - return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) - } - dbExternalAccountKeyReference := new(dbExternalAccountKeyReference) - if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) - } - - return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) -} - -func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { - old, err := db.getDBExternalAccountKey(ctx, eak.ID) - if err != nil { - return err - } - - if old.ProvisionerID != provisionerID { - return errors.New("provisioner does not match provisioner for which the EAB key was created") - } - - if old.ProvisionerID != eak.ProvisionerID { - return errors.New("cannot change provisioner for an existing ACME EAB Key") - } - - if old.Reference != eak.Reference { - return errors.New("cannot change reference for an existing ACME EAB Key") - } - - nu := dbExternalAccountKey{ - ID: eak.ID, - ProvisionerID: eak.ProvisionerID, - Reference: eak.Reference, - AccountID: eak.AccountID, - KeyBytes: eak.KeyBytes, - CreatedAt: eak.CreatedAt, - BoundAt: eak.BoundAt, - } - - return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) -} - -func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { - referencesByProvisionerIndexMux.Lock() - defer referencesByProvisionerIndexMux.Unlock() - - var eakIDs []string - b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) - if err != nil { - if !nosqlDB.IsErrNotFound(err) { - return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) - } - } else { - if err := json.Unmarshal(b, &eakIDs); err != nil { - return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) - } - } - - var newEAKIDs []string - newEAKIDs = append(newEAKIDs, eakIDs...) - newEAKIDs = append(newEAKIDs, eakID) - var ( - _old interface{} = eakIDs - _new interface{} = newEAKIDs - ) - - if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { - return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) - } - - return nil -} - -func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error { - referencesByProvisionerIndexMux.Lock() - defer referencesByProvisionerIndexMux.Unlock() - - var eakIDs []string - b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID)) - if err != nil { - if !nosqlDB.IsErrNotFound(err) { - return errors.Wrapf(err, "error loading reference IDs for provisioner %s", provisionerID) - } - } else { - if err := json.Unmarshal(b, &eakIDs); err != nil { - return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) - } - } - - newEAKIDs := removeElement(eakIDs, eakID) - var ( - _old interface{} = eakIDs - _new interface{} = newEAKIDs - ) - - if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil { - return errors.Wrapf(err, "error saving referenceIDs index for provisioner %s", provisionerID) - } - - return nil -} - -// referenceKey returns a unique key for a reference per provisioner -func referenceKey(provisionerID, reference string) string { - return provisionerID + "." + reference -} - -// sliceIndex finds the index of item in slice -func sliceIndex(slice []string, item string) int { - for i := range slice { - if slice[i] == item { - return i - } - } - return -1 -} - -// removeElement deletes the item if it exists in the -// slice. It returns a new slice, keeping the old one intact. -func removeElement(slice []string, item string) []string { - - newSlice := make([]string, 0) - index := sliceIndex(slice, item) - if index < 0 { - newSlice = append(newSlice, slice...) - return newSlice - } - - newSlice = append(newSlice, slice[:index]...) - - return append(newSlice, slice[index+1:]...) -} diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 77937c2e..83a23476 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -3,7 +3,6 @@ package nosql import ( "context" "encoding/json" - "fmt" "testing" "time" @@ -697,1087 +696,3 @@ func TestDB_UpdateAccount(t *testing.T) { }) } } - -func TestDB_getDBExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - dbeak *dbExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - err: nil, - dbeak: dbeak, - } - }, - "fail/not-found": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return nil, nosqldb.ErrNotFound - }, - }, - err: acme.ErrNotFound, - } - }, - "fail/db.Get-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return nil, errors.New("force") - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - "fail/unmarshal-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return []byte("foo"), nil - }, - }, - err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, dbeak.ID, tc.dbeak.ID) - assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) - assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID) - assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) - assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) - assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) - assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) - } - }) - } -} - -func TestDB_GetExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - eak *acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - eak: &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - } - }, - "fail/db.Get-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return nil, errors.New("force") - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - "fail/non-matching-provisioner": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: "aDifferentProvID", - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - eak: &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, eak.ID, tc.eak.ID) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) - assert.Equals(t, eak.Reference, tc.eak.Reference) - assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, eak.AccountID, tc.eak.AccountID) - assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) - } - }) - } -} - -func TestDB_GetExternalAccountKeyByReference(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - ref string - acmeErr *acme.Error - eak *acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - }, - eak: &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - err: nil, - } - }, - "ok/no-reference": func(t *testing.T) test { - return test{ - ref: "", - eak: nil, - err: nil, - } - }, - "fail/reference-not-found": func(t *testing.T) test { - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), provID+"."+ref) - return nil, nosqldb.ErrNotFound - }, - }, - err: errors.New("not found"), - } - }, - "fail/reference-load-error": func(t *testing.T) test { - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), provID+"."+ref) - return nil, errors.New("force") - }, - }, - err: errors.New("error loading ACME EAB key for reference ref: force"), - } - }, - "fail/reference-unmarshal-error": func(t *testing.T) test { - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) - assert.Equals(t, string(key), provID+"."+ref) - return []byte{0}, nil - }, - }, - err: errors.New("error unmarshaling ACME EAB key for reference ref"), - } - }, - "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - ref: ref, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return nil, errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } - } else if assert.Nil(t, tc.err) && tc.eak != nil { - assert.Equals(t, eak.ID, tc.eak.ID) - assert.Equals(t, eak.AccountID, tc.eak.AccountID) - assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) - assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) - assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) - assert.Equals(t, eak.Reference, tc.eak.Reference) - } - }) - } -} - -func TestDB_GetExternalAccountKeys(t *testing.T) { - keyID1 := "keyID1" - keyID2 := "keyID2" - keyID3 := "keyID3" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - eaks []*acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak1 := &dbExternalAccountKey{ - ID: keyID1, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b1, err := json.Marshal(dbeak1) - assert.FatalError(t, err) - dbeak2 := &dbExternalAccountKey{ - ID: keyID2, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b2, err := json.Marshal(dbeak2) - assert.FatalError(t, err) - dbeak3 := &dbExternalAccountKey{ - ID: keyID3, - ProvisionerID: "aDifferentProvID", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b3, err := json.Marshal(dbeak3) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - keys := []string{keyID1, keyID2} - b, err := json.Marshal(keys) - assert.FatalError(t, err) - return b, nil - case string(externalAccountKeyTable): - switch string(key) { - case keyID1: - return b1, nil - case keyID2: - return b2, nil - default: - assert.FatalError(t, errors.Errorf("unexpected key %s", string(key))) - return nil, errors.New("force") - } - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - // TODO: remove the MList - MList: func(bucket []byte) ([]*nosqldb.Entry, error) { - switch string(bucket) { - case string(externalAccountKeyTable): - return []*nosqldb.Entry{ - { - Bucket: bucket, - Key: []byte(keyID1), - Value: b1, - }, - { - Bucket: bucket, - Key: []byte(keyID2), - Value: b2, - }, - { - Bucket: bucket, - Key: []byte(keyID3), - Value: b3, - }, - }, nil - case string(externalAccountKeysByProvisionerIDTable): - keys := []string{keyID1, keyID2} - b, err := json.Marshal(keys) - assert.FatalError(t, err) - return []*nosqldb.Entry{ - { - Bucket: bucket, - Key: []byte(provID), - Value: b, - }, - }, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force default") - } - }, - }, - eaks: []*acme.ExternalAccountKey{ - { - ID: keyID1, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - { - ID: keyID2, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - }, - }, - } - }, - "fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) - return nil, errors.New("force") - }, - }, - err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"), - } - }, - "fail/db.getDBExternalAccountKey": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - keys := []string{keyID1, keyID2} - b, err := json.Marshal(keys) - assert.FatalError(t, err) - return b, nil - case string(externalAccountKeyTable): - return nil, errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force bucket") - } - }, - }, - err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if eaks, err := d.GetExternalAccountKeys(context.Background(), provID); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.Equals(t, tc.err.Error(), err.Error()) - } - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, len(eaks), len(tc.eaks)) - for i, eak := range eaks { - assert.Equals(t, eak.ID, tc.eaks[i].ID) - assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) - assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID) - assert.Equals(t, eak.Reference, tc.eaks[i].Reference) - assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) - assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) - assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) - } - } - }) - } -} - -func TestDB_DeleteExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - acmeErr *acme.Error - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - b, err := json.Marshal([]string{keyID}) - assert.FatalError(t, err) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force default") - } - }, - MDel: func(bucket, key []byte) error { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return errors.New("force default") - } - }, - MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) { - fmt.Println(string(bucket)) - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, provID+"."+ref, string(key)) - return nil, true, nil - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - return nil, true, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force default") - } - }, - }, - } - }, - "fail/not-found": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeyTable)) - assert.Equals(t, string(key), keyID) - return nil, nosqldb.ErrNotFound - }, - }, - err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"), - } - }, - "fail/non-matching-provisioner": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: "aDifferentProvID", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeyTable)) - assert.Equals(t, string(key), keyID) - return b, nil - }, - }, - err: errors.New("provisioner does not match provisioner for which the EAB key was created"), - } - }, - "fail/delete-reference": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force default") - } - }, - MDel: func(bucket, key []byte) error { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return errors.New("force") - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return errors.New("force default") - } - }, - }, - err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"), - } - }, - "fail/delete-eak": func(t *testing.T) test { - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - dbref := &dbExternalAccountKeyReference{ - Reference: ref, - ExternalAccountKeyID: keyID, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - dbrefBytes, err := json.Marshal(dbref) - assert.FatalError(t, err) - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) - return dbrefBytes, nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return b, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, errors.New("force") - } - }, - MDel: func(bucket, key []byte) error { - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), provID+"."+ref) - return nil - case string(externalAccountKeyTable): - assert.Equals(t, string(key), keyID) - return errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return errors.New("force") - } - }, - }, - err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil { - switch k := err.(type) { - case *acme.Error: - if assert.NotNil(t, tc.acmeErr) { - assert.Equals(t, k.Type, tc.acmeErr.Type) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - assert.Equals(t, k.Status, tc.acmeErr.Status) - assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) - assert.Equals(t, k.Detail, tc.acmeErr.Detail) - } - default: - if assert.NotNil(t, tc.err) { - assert.Equals(t, err.Error(), tc.err.Error()) - } - } - } else { - assert.Nil(t, tc.err) - } - }) - } -} - -func TestDB_CreateExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - type test struct { - db nosql.DB - err error - _id *string - eak *acme.ExternalAccountKey - } - var tests = map[string]func(t *testing.T) test{ - "ok": func(t *testing.T) test { - var ( - id string - idPtr = &id - ) - now := clock.Now() - eak := &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: "ref", - AccountID: "", - CreatedAt: now, - } - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) - assert.Equals(t, provID, string(key)) - b, _ := json.Marshal([]string{}) - return b, nil - }, - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - return nu, true, nil - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, provID+"."+ref, string(key)) - assert.Equals(t, nil, old) - return nu, true, nil - case string(externalAccountKeyTable): - assert.Equals(t, nil, old) - - id = string(key) - - dbeak := new(dbExternalAccountKey) - assert.FatalError(t, json.Unmarshal(nu, dbeak)) - assert.Equals(t, string(key), dbeak.ID) - assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID) - assert.Equals(t, eak.Reference, dbeak.Reference) - assert.Equals(t, 32, len(dbeak.KeyBytes)) - assert.False(t, dbeak.CreatedAt.IsZero()) - assert.Equals(t, dbeak.AccountID, eak.AccountID) - assert.True(t, dbeak.BoundAt.IsZero()) - return nu, true, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force default") - } - }, - }, - eak: eak, - _id: idPtr, - } - }, - "fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - - switch string(bucket) { - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, string(key), ref) - assert.Equals(t, old, nil) - return nu, true, nil - case string(externalAccountKeyTable): - assert.Equals(t, old, nil) - return nu, true, errors.New("force") - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force") - } - }, - }, - err: errors.New("error saving acme external_account_key: force"), - } - }, - "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { - return test{ - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable)) - assert.Equals(t, provID, string(key)) - b, _ := json.Marshal([]string{}) - return b, nil - }, - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - switch string(bucket) { - case string(externalAccountKeysByProvisionerIDTable): - assert.Equals(t, provID, string(key)) - return nu, true, nil - case string(externalAccountKeysByReferenceTable): - assert.Equals(t, provID+"."+ref, string(key)) - assert.Equals(t, old, nil) - return nu, true, errors.New("force") - case string(externalAccountKeyTable): - assert.Equals(t, old, nil) - return nu, true, nil - default: - assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) - return nil, false, errors.New("force") - } - }, - }, - err: errors.New("error saving acme external_account_key_reference: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref) - fmt.Println(name, err) - if err != nil { - if assert.NotNil(t, tc.err) { - assert.Equals(t, err.Error(), tc.err.Error()) - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, *tc._id, eak.ID) - assert.Equals(t, provID, eak.ProvisionerID) - assert.Equals(t, ref, eak.Reference) - assert.Equals(t, "", eak.AccountID) - assert.False(t, eak.CreatedAt.IsZero()) - assert.False(t, eak.AlreadyBound()) - assert.True(t, eak.BoundAt.IsZero()) - } - }) - } -} - -func TestDB_UpdateExternalAccountKey(t *testing.T) { - keyID := "keyID" - provID := "provID" - ref := "ref" - now := clock.Now() - dbeak := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(dbeak) - assert.FatalError(t, err) - type test struct { - db nosql.DB - eak *acme.ExternalAccountKey - err error - } - var tests = map[string]func(t *testing.T) test{ - - "ok": func(t *testing.T) test { - eak := &acme.ExternalAccountKey{ - ID: keyID, - ProvisionerID: provID, - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - return test{ - eak: eak, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return b, nil - }, - MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, old, b) - - dbNew := new(dbExternalAccountKey) - assert.FatalError(t, json.Unmarshal(nu, dbNew)) - assert.Equals(t, dbNew.ID, dbeak.ID) - assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID) - assert.Equals(t, dbNew.Reference, dbeak.Reference) - assert.Equals(t, dbNew.AccountID, dbeak.AccountID) - assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) - assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) - assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) - return nu, true, nil - }, - }, - } - }, - "fail/provisioner-mismatch": func(t *testing.T) test { - newDBEAK := &dbExternalAccountKey{ - ID: keyID, - ProvisionerID: "aDifferentProvID", - Reference: ref, - AccountID: "", - KeyBytes: []byte{1, 3, 3, 7}, - CreatedAt: now, - } - b, err := json.Marshal(newDBEAK) - assert.FatalError(t, err) - return test{ - eak: &acme.ExternalAccountKey{ - ID: keyID, - }, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return b, nil - }, - }, - err: errors.New("provisioner does not match provisioner for which the EAB key was created"), - } - }, - "fail/db.Get-error": func(t *testing.T) test { - return test{ - eak: &acme.ExternalAccountKey{ - ID: keyID, - }, - db: &db.MockNoSQLDB{ - MGet: func(bucket, key []byte) ([]byte, error) { - assert.Equals(t, bucket, externalAccountKeyTable) - assert.Equals(t, string(key), keyID) - - return nil, errors.New("force") - }, - }, - err: errors.New("error loading external account key keyID: force"), - } - }, - } - for name, run := range tests { - tc := run(t) - t.Run(name, func(t *testing.T) { - d := DB{db: tc.db} - if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil { - if assert.NotNil(t, tc.err) { - assert.HasPrefix(t, err.Error(), tc.err.Error()) - } - } else if assert.Nil(t, tc.err) { - assert.Equals(t, dbeak.ID, tc.eak.ID) - assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID) - assert.Equals(t, dbeak.Reference, tc.eak.Reference) - assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) - assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) - assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) - assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) - } - }) - } -} diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go new file mode 100644 index 00000000..6b6334a5 --- /dev/null +++ b/acme/db/nosql/eab.go @@ -0,0 +1,306 @@ +package nosql + +import ( + "context" + "crypto/rand" + "encoding/json" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" + nosqlDB "github.com/smallstep/nosql" +) + +// CreateExternalAccountKey creates a new External Account Binding key with a name +func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + keyID, err := randID() + if err != nil { + return nil, err + } + + random := make([]byte, 32) + _, err = rand.Read(random) + if err != nil { + return nil, err + } + + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provisionerID, + Reference: reference, + KeyBytes: random, + CreatedAt: clock.Now(), + } + + if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { + return nil, err + } + + if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil { + return nil, err + } + + if dbeak.Reference != "" { + dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ + Reference: dbeak.Reference, + ExternalAccountKeyID: dbeak.ID, + } + if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { + return nil, err + } + } + + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} + +// GetExternalAccountKey retrieves an External Account Binding key by KeyID +func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) + if err != nil { + return nil, err + } + + if dbeak.ProvisionerID != provisionerID { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created") + } + + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} + +func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) + if err != nil { + return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) + } + + if dbeak.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") + } + + if dbeak.Reference != "" { + if err := db.db.Del(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference) + } + } + if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) + } + if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil { + return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID) + } + + return nil +} + +// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) { + + // TODO: mutex? + + var eakIDs []string + r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(r, &eakIDs); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) + } + } + + keys := []*acme.ExternalAccountKey{} + for _, eakID := range eakIDs { + if eakID == "" { + continue // shouldn't happen; just in case + } + eak, err := db.getDBExternalAccountKey(ctx, eakID) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return nil, errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID) + } + } + keys = append(keys, &acme.ExternalAccountKey{ + ID: eak.ID, + KeyBytes: eak.KeyBytes, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + }) + } + + return keys, nil +} + +// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference +func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + if reference == "" { + return nil, nil + } + + k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, reference))) + if nosqlDB.IsErrNotFound(err) { + return nil, acme.ErrNotFound + } else if err != nil { + return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) + } + dbExternalAccountKeyReference := new(dbExternalAccountKeyReference) + if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) + } + + return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) +} + +func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + old, err := db.getDBExternalAccountKey(ctx, eak.ID) + if err != nil { + return err + } + + if old.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") + } + + if old.ProvisionerID != eak.ProvisionerID { + return errors.New("cannot change provisioner for an existing ACME EAB Key") + } + + if old.Reference != eak.Reference { + return errors.New("cannot change reference for an existing ACME EAB Key") + } + + nu := dbExternalAccountKey{ + ID: eak.ID, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + } + + return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) +} + +func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMux.Lock() + defer referencesByProvisionerIndexMux.Unlock() + + if eakID == "" { + return errors.Errorf("can't add empty eakID for provisioner %s", provisionerID) + } + + var eakIDs []string + b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + for _, id := range eakIDs { + if id == eakID { + // return an error when a duplicate ID is found + return errors.Errorf("eakID %s already exists for provisioner %s", eakID, provisionerID) + } + } + + var newEAKIDs []string + newEAKIDs = append(newEAKIDs, eakIDs...) + newEAKIDs = append(newEAKIDs, eakID) + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + + return nil +} + +func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMux.Lock() + defer referencesByProvisionerIndexMux.Unlock() + + var eakIDs []string + b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + newEAKIDs := removeElement(eakIDs, eakID) + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + + return nil +} + +// referenceKey returns a unique key for a reference per provisioner +func referenceKey(provisionerID, reference string) string { + return provisionerID + "." + reference +} + +// sliceIndex finds the index of item in slice +func sliceIndex(slice []string, item string) int { + for i := range slice { + if slice[i] == item { + return i + } + } + return -1 +} + +// removeElement deletes the item if it exists in the +// slice. It returns a new slice, keeping the old one intact. +func removeElement(slice []string, item string) []string { + + newSlice := make([]string, 0) + index := sliceIndex(slice, item) + if index < 0 { + newSlice = append(newSlice, slice...) + return newSlice + } + + newSlice = append(newSlice, slice[:index]...) + + return append(newSlice, slice[index+1:]...) +} diff --git a/acme/db/nosql/eab_test.go b/acme/db/nosql/eab_test.go new file mode 100644 index 00000000..6db0b734 --- /dev/null +++ b/acme/db/nosql/eab_test.go @@ -0,0 +1,1710 @@ +package nosql + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + certdb "github.com/smallstep/certificates/db" + "github.com/smallstep/nosql" + nosqldb "github.com/smallstep/nosql/database" +) + +func TestDB_getDBExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + dbeak *dbExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: nil, + dbeak: dbeak, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: acme.ErrNotFound, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return []byte("foo"), nil + }, + }, + err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.dbeak.ID) + assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) + assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID) + assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) + assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) + assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) + assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) + } + }) + } +} + +func TestDB_GetExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, eak.Reference, tc.eak.Reference) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + } + }) + } +} + +func TestDB_GetExternalAccountKeyByReference(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + ref string + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + err: nil, + } + }, + "ok/no-reference": func(t *testing.T) test { + return test{ + ref: "", + eak: nil, + err: nil, + } + }, + "fail/reference-not-found": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("not found"), + } + }, + "fail/reference-load-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB key for reference ref: force"), + } + }, + "fail/reference-unmarshal-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return []byte{0}, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB key for reference ref"), + } + }, + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) && tc.eak != nil { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, eak.Reference, tc.eak.Reference) + } + }) + } +} + +func TestDB_GetExternalAccountKeys(t *testing.T) { + keyID1 := "keyID1" + keyID2 := "keyID2" + keyID3 := "keyID3" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak1 := &dbExternalAccountKey{ + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b1, err := json.Marshal(dbeak1) + assert.FatalError(t, err) + dbeak2 := &dbExternalAccountKey{ + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b2, err := json.Marshal(dbeak2) + assert.FatalError(t, err) + dbeak3 := &dbExternalAccountKey{ + ID: keyID3, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b3, err := json.Marshal(dbeak3) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{"", keyID1, keyID2} // includes an empty keyID + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + switch string(key) { + case keyID1: + return b1, nil + case keyID2: + return b2, nil + default: + assert.FatalError(t, errors.Errorf("unexpected key %s", string(key))) + return nil, errors.New("force default") + } + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + // TODO: remove the MList + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + switch string(bucket) { + case string(externalAccountKeyTable): + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: b1, + }, + { + Bucket: bucket, + Key: []byte(keyID2), + Value: b2, + }, + { + Bucket: bucket, + Key: []byte(keyID3), + Value: b3, + }, + }, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(provID), + Value: b, + }, + }, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + }, + eaks: []*acme.ExternalAccountKey{ + { + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + { + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + }, + } + }, + "fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"), + } + }, + "fail/db.Get-externalAccountKeysByProvisionerIDTable-unmarshal": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB Key IDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/db.getDBExternalAccountKey": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force bucket") + } + }, + }, + err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if eaks, err := d.GetExternalAccountKeys(context.Background(), provID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.Equals(t, tc.err.Error(), err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, len(eaks), len(tc.eaks)) + for i, eak := range eaks { + assert.Equals(t, eak.ID, tc.eaks[i].ID) + assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID) + assert.Equals(t, eak.Reference, tc.eaks[i].Reference) + assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) + assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) + assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) + } + } + }) + } +} + +func TestDB_DeleteExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + b, err := json.Marshal([]string{keyID}) + assert.FatalError(t, err) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) { + fmt.Println(string(bucket)) + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + return nil, true, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nil, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/delete-reference": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"), + } + }, + "fail/delete-eak": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"), + } + }, + "fail/delete-eakID": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + return b, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error removing ACME EAB Key ID keyID: error loading eakIDs for provisioner provID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.Equals(t, err.Error(), tc.err.Error()) + } + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestDB_CreateExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + _id *string + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + var ( + id string + idPtr = &id + ) + now := clock.Now() + eak := &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + CreatedAt: now, + } + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + assert.Equals(t, nil, old) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, nil, old) + + id = string(key) + + dbeak := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbeak)) + assert.Equals(t, string(key), dbeak.ID) + assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID) + assert.Equals(t, eak.Reference, dbeak.Reference) + assert.Equals(t, 32, len(dbeak.KeyBytes)) + assert.False(t, dbeak.CreatedAt.IsZero()) + assert.Equals(t, dbeak.AccountID, eak.AccountID) + assert.True(t, dbeak.BoundAt.IsZero()) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + eak: eak, + _id: idPtr, + } + }, + "fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error saving acme external_account_key: force"), + } + }, + "fail/addEAKID-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + return nil, errors.New("force") + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error saving acme external_account_key_reference: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.Equals(t, err.Error(), tc.err.Error()) + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, *tc._id, eak.ID) + assert.Equals(t, provID, eak.ProvisionerID) + assert.Equals(t, ref, eak.Reference) + assert.Equals(t, "", eak.AccountID) + assert.False(t, eak.CreatedAt.IsZero()) + assert.False(t, eak.AlreadyBound()) + assert.True(t, eak.BoundAt.IsZero()) + } + }) + } +} + +func TestDB_UpdateExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + type test struct { + db nosql.DB + eak *acme.ExternalAccountKey + err error + } + var tests = map[string]func(t *testing.T) test{ + + "ok": func(t *testing.T) test { + eak := &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + return test{ + eak: eak, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, old, b) + + dbNew := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbNew)) + assert.Equals(t, dbNew.ID, dbeak.ID) + assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID) + assert.Equals(t, dbNew.Reference, dbeak.Reference) + assert.Equals(t, dbNew.AccountID, dbeak.AccountID) + assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) + assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) + assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) + return nu, true, nil + }, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/provisioner-mismatch": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + }, + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/provisioner-change": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvisionerID", + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("cannot change provisioner for an existing ACME EAB Key"), + } + }, + "fail/reference-change": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "aDifferentReference", + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("cannot change reference for an existing ACME EAB Key"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.eak.ID) + assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, dbeak.Reference, tc.eak.Reference) + assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) + assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) + } + }) + } +} + +func TestDB_addEAKID(t *testing.T) { + provID := "provID" + eakID := "eakID" + type test struct { + ctx context.Context + provisionerID string + eakID string + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "fail/empty-eakID": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: "", + err: errors.New("can't add empty eakID for provisioner provID"), + } + }, + "fail/db.Get": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/unmarshal": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/eakID-already-exists": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{eakID}) + return b, nil + }, + }, + err: errors.New("eakID eakID already exists for provisioner provID"), + } + }, + "fail/db.save": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", eakID}) + assert.Equals(t, nu, newB) + return newB, true, errors.New("force") + }, + }, + err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"), + } + }, + "ok/db.Get-not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, nosqldb.ErrNotFound + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + assert.Equals(t, old, []byte{110, 117, 108, 108}) + b, _ := json.Marshal([]string{eakID}) + assert.Equals(t, nu, b) + return b, true, nil + }, + }, + err: nil, + } + }, + "ok": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", "id2"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", "id2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", "id2", eakID}) + assert.Equals(t, nu, newB) + return newB, true, nil + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := &DB{ + db: tc.db, + } + wantErr := tc.err != nil + err := db.addEAKID(tc.ctx, tc.provisionerID, tc.eakID) + if (err != nil) != wantErr { + t.Errorf("DB.addEAKID() error = %v, wantErr %v", err, wantErr) + } + if err != nil { + assert.Equals(t, tc.err.Error(), err.Error()) + } + }) + } +} + +func TestDB_deleteEAKID(t *testing.T) { + provID := "provID" + eakID := "eakID" + type test struct { + ctx context.Context + provisionerID string + eakID string + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "fail/db.Get": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/unmarshal": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/db.save": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", eakID}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", eakID}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1"}) + assert.Equals(t, nu, newB) + return newB, true, errors.New("force") + }, + }, + err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"), + } + }, + "ok/db.Get-not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, nosqldb.ErrNotFound + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + assert.Equals(t, old, []byte{110, 117, 108, 108}) + b, _ := json.Marshal([]string{}) + assert.Equals(t, nu, b) + return b, true, nil + }, + }, + err: nil, + } + }, + "ok": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", eakID, "id2"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", eakID, "id2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", "id2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := &DB{ + db: tc.db, + } + wantErr := tc.err != nil + err := db.deleteEAKID(tc.ctx, tc.provisionerID, tc.eakID) + if (err != nil) != wantErr { + t.Errorf("DB.deleteEAKID() error = %v, wantErr %v", err, wantErr) + } + if err != nil { + assert.Equals(t, tc.err.Error(), err.Error()) + } + }) + } +} + +func TestDB_addAndDeleteEAKID(t *testing.T) { + provID := "provID" + callCounter := 0 + type test struct { + ctx context.Context + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "ok/multi": func(t *testing.T) test { + return test{ + ctx: context.Background(), + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + switch callCounter { + case 0: + return nil, nosqldb.ErrNotFound + case 1: + b, _ := json.Marshal([]string{"eakID"}) + return b, nil + case 2: + b, _ := json.Marshal([]string{}) + return b, nil + case 3: + b, _ := json.Marshal([]string{"eakID1"}) + return b, nil + case 4: + b, _ := json.Marshal([]string{"eakID1", "eakID2"}) + return b, nil + case 5: + b, _ := json.Marshal([]string{"eakID2"}) + return b, nil + default: + assert.FatalError(t, errors.New("unexpected get iteration")) + return nil, errors.New("force get default") + } + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + switch callCounter { + case 0: + assert.Equals(t, old, []byte{110, 117, 108, 108}) + newB, _ := json.Marshal([]string{"eakID"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 1: + oldB, _ := json.Marshal([]string{"eakID"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{}) + return newB, true, nil + case 2: + oldB, _ := json.Marshal([]string{}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID1"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 3: + oldB, _ := json.Marshal([]string{"eakID1"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID1", "eakID2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 4: + oldB, _ := json.Marshal([]string{"eakID1", "eakID2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 5: + oldB, _ := json.Marshal([]string{"eakID2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{}) + assert.Equals(t, nu, newB) + return newB, true, nil + default: + assert.FatalError(t, errors.New("unexpected get iteration")) + return nil, true, errors.New("force save default") + } + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + + // goal of this test is to simulate multiple calls; no errors expected. + + db := &DB{ + db: tc.db, + } + + err := db.addEAKID(tc.ctx, provID, "eakID") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID") + if err != nil { + t.Errorf("DB.deleteEAKID() error = %v", err) + } + + callCounter++ + err = db.addEAKID(tc.ctx, provID, "eakID1") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.addEAKID(tc.ctx, provID, "eakID2") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID1") + if err != nil { + t.Errorf("DB.deleteEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID2") + if err != nil { + t.Errorf("DB.deleteAKID() error = %v", err) + } + }) + } +} + +func Test_removeElement(t *testing.T) { + tests := []struct { + name string + slice []string + item string + want []string + }{ + { + name: "remove-first", + slice: []string{"id1", "id2", "id3"}, + item: "id1", + want: []string{"id2", "id3"}, + }, + { + name: "remove-last", + slice: []string{"id1", "id2", "id3"}, + item: "id3", + want: []string{"id1", "id2"}, + }, + { + name: "remove-middle", + slice: []string{"id1", "id2", "id3"}, + item: "id2", + want: []string{"id1", "id3"}, + }, + { + name: "remove-non-existing", + slice: []string{"id1", "id2", "id3"}, + item: "none", + want: []string{"id1", "id2", "id3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := removeElement(tt.slice, tt.item) + if !cmp.Equal(tt.want, got) { + t.Errorf("removeElement() diff =\n %s", cmp.Diff(tt.want, got)) + } + }) + } +} diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 8343c196..2de82b70 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -11,18 +11,18 @@ import ( ) var ( - accountTable = []byte("acme_accounts") - accountByKeyIDTable = []byte("acme_keyID_accountID_index") - authzTable = []byte("acme_authzs") - challengeTable = []byte("acme_challenges") - nonceTable = []byte("nonces") - orderTable = []byte("acme_orders") - ordersByAccountIDTable = []byte("acme_account_orders_index") - certTable = []byte("acme_certs") - certBySerialTable = []byte("acme_serial_certs_index") - externalAccountKeyTable = []byte("acme_external_account_keys") - externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") - externalAccountKeysByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") + accountTable = []byte("acme_accounts") + accountByKeyIDTable = []byte("acme_keyID_accountID_index") + authzTable = []byte("acme_authzs") + challengeTable = []byte("acme_challenges") + nonceTable = []byte("nonces") + orderTable = []byte("acme_orders") + ordersByAccountIDTable = []byte("acme_account_orders_index") + certTable = []byte("acme_certs") + certBySerialTable = []byte("acme_serial_certs_index") + externalAccountKeyTable = []byte("acme_external_account_keys") + externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") + externalAccountKeyIDsByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") ) // DB is a struct that implements the AcmeDB interface. @@ -35,7 +35,7 @@ func New(db nosqlDB.DB) (*DB, error) { tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, certBySerialTable, externalAccountKeyTable, - externalAccountKeysByReferenceTable, externalAccountKeysByProvisionerIDTable, + externalAccountKeysByReferenceTable, externalAccountKeyIDsByProvisionerIDTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index f7de9290..475e6606 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -391,6 +391,54 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) { } } +func Test_provisionerFromContext(t *testing.T) { + prov := &linkedca.Provisioner{ + Id: "provID", + Name: "acmeProv", + } + tests := []struct { + name string + ctx context.Context + want *linkedca.Provisioner + wantErr bool + }{ + { + name: "fail/no-provisioner", + ctx: context.Background(), + want: nil, + wantErr: true, + }, + { + name: "fail/wrong-type", + ctx: context.WithValue(context.Background(), provisionerContextKey, "prov"), + want: nil, + wantErr: true, + }, + { + name: "ok", + ctx: context.WithValue(context.Background(), provisionerContextKey, prov), + want: &linkedca.Provisioner{ + Id: "provID", + Name: "acmeProv", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := provisionerFromContext(tt.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("provisionerFromContext() error = %v, wantErr %v", err, tt.wantErr) + return + } + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{})} + if !cmp.Equal(tt.want, got, opts...) { + t.Errorf("provisionerFromContext() diff =\n %s", cmp.Diff(tt.want, got, opts...)) + } + }) + } +} + func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { type fields struct { Reference string @@ -488,6 +536,28 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { }, } }, + "fail/no-provisioner-in-context": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "aRef", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: ctx, + body: body, + statusCode: 500, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error getting provisioner from context: provisioner expected in request context", + }, + } + }, "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") @@ -759,6 +829,21 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { err *admin.Error } var tests = map[string]func(t *testing.T) test{ + "fail/no-provisioner-in-context": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + return test{ + ctx: ctx, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error getting provisioner from context: provisioner expected in request context", + }, + } + }, "fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") @@ -861,6 +946,23 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { err *admin.Error } var tests = map[string]func(t *testing.T) test{ + "fail/no-provisioner-in-context": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := httptest.NewRequest("GET", "/foo", nil) + return test{ + ctx: ctx, + statusCode: 500, + req: req, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error getting provisioner from context: provisioner expected in request context", + }, + } + }, "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName")