2021-02-25 18:24:24 +00:00
|
|
|
package nosql
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-07-17 15:35:44 +00:00
|
|
|
"crypto/rand"
|
2021-02-25 18:24:24 +00:00
|
|
|
"encoding/json"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2021-03-01 06:49:20 +00:00
|
|
|
"github.com/smallstep/certificates/acme"
|
2021-02-25 18:24:24 +00:00
|
|
|
nosqlDB "github.com/smallstep/nosql"
|
|
|
|
"go.step.sm/crypto/jose"
|
|
|
|
)
|
|
|
|
|
|
|
|
// dbAccount represents an ACME account.
|
|
|
|
type dbAccount struct {
|
2021-03-19 22:01:26 +00:00
|
|
|
ID string `json:"id"`
|
|
|
|
Key *jose.JSONWebKey `json:"key"`
|
|
|
|
Contact []string `json:"contact,omitempty"`
|
|
|
|
Status acme.Status `json:"status"`
|
2021-03-29 19:04:14 +00:00
|
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
|
|
DeactivatedAt time.Time `json:"deactivatedAt"`
|
2021-02-25 18:24:24 +00:00
|
|
|
}
|
|
|
|
|
2021-02-26 18:12:30 +00:00
|
|
|
func (dba *dbAccount) clone() *dbAccount {
|
|
|
|
nu := *dba
|
|
|
|
return &nu
|
|
|
|
}
|
|
|
|
|
2021-07-17 15:35:44 +00:00
|
|
|
type dbExternalAccountKey struct {
|
2021-09-16 21:09:24 +00:00
|
|
|
ID string `json:"id"`
|
|
|
|
Provisioner string `json:"provisioner"`
|
|
|
|
Reference string `json:"reference"`
|
|
|
|
AccountID string `json:"accountID,omitempty"`
|
|
|
|
KeyBytes []byte `json:"key"`
|
|
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
|
|
BoundAt time.Time `json:"boundAt"`
|
2021-07-17 15:35:44 +00:00
|
|
|
}
|
|
|
|
|
2021-09-17 15:08:02 +00:00
|
|
|
type dbExternalAccountKeyReference struct {
|
|
|
|
Reference string `json:"reference"`
|
|
|
|
ExternalAccountKeyID string `json:"externalAccountKeyID"`
|
|
|
|
}
|
|
|
|
|
2021-03-22 21:46:05 +00:00
|
|
|
func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) {
|
|
|
|
id, err := db.db.Get(accountByKeyIDTable, []byte(kid))
|
|
|
|
if err != nil {
|
|
|
|
if nosqlDB.IsErrNotFound(err) {
|
2021-03-25 21:54:12 +00:00
|
|
|
return "", acme.ErrNotFound
|
2021-03-22 21:46:05 +00:00
|
|
|
}
|
|
|
|
return "", errors.Wrapf(err, "error loading key-account index for key %s", kid)
|
|
|
|
}
|
|
|
|
return string(id), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getDBAccount retrieves and unmarshals dbAccount.
|
|
|
|
func (db *DB) getDBAccount(ctx context.Context, id string) (*dbAccount, error) {
|
|
|
|
data, err := db.db.Get(accountTable, []byte(id))
|
|
|
|
if err != nil {
|
|
|
|
if nosqlDB.IsErrNotFound(err) {
|
2021-03-25 21:54:12 +00:00
|
|
|
return nil, acme.ErrNotFound
|
2021-03-22 21:46:05 +00:00
|
|
|
}
|
|
|
|
return nil, errors.Wrapf(err, "error loading account %s", id)
|
|
|
|
}
|
|
|
|
|
|
|
|
dbacc := new(dbAccount)
|
|
|
|
if err = json.Unmarshal(data, dbacc); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error unmarshaling account %s into dbAccount", id)
|
|
|
|
}
|
|
|
|
return dbacc, nil
|
|
|
|
}
|
|
|
|
|
2021-07-17 17:02:47 +00:00
|
|
|
// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey.
|
|
|
|
func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) {
|
|
|
|
data, err := db.db.Get(externalAccountKeyTable, []byte(id))
|
|
|
|
if err != nil {
|
|
|
|
if nosqlDB.IsErrNotFound(err) {
|
|
|
|
return nil, acme.ErrNotFound
|
|
|
|
}
|
|
|
|
return nil, errors.Wrapf(err, "error loading external account key %s", id)
|
|
|
|
}
|
|
|
|
|
|
|
|
dbeak := new(dbExternalAccountKey)
|
|
|
|
if err = json.Unmarshal(data, dbeak); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", id)
|
|
|
|
}
|
|
|
|
|
|
|
|
return dbeak, nil
|
|
|
|
}
|
|
|
|
|
2021-03-22 21:46:05 +00:00
|
|
|
// GetAccount retrieves an ACME account by ID.
|
|
|
|
func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) {
|
|
|
|
dbacc, err := db.getDBAccount(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &acme.Account{
|
|
|
|
Status: dbacc.Status,
|
|
|
|
Contact: dbacc.Contact,
|
|
|
|
Key: dbacc.Key,
|
|
|
|
ID: dbacc.ID,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAccountByKeyID retrieves an ACME account by KeyID (thumbprint of the Account Key -- JWK).
|
|
|
|
func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*acme.Account, error) {
|
|
|
|
id, err := db.getAccountIDByKeyID(ctx, kid)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return db.GetAccount(ctx, id)
|
|
|
|
}
|
|
|
|
|
2021-02-25 18:24:24 +00:00
|
|
|
// CreateAccount imlements the AcmeDB.CreateAccount interface.
|
2021-03-01 06:49:20 +00:00
|
|
|
func (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error {
|
|
|
|
var err error
|
2021-02-28 01:05:37 +00:00
|
|
|
acc.ID, err = randID()
|
2021-02-25 18:24:24 +00:00
|
|
|
if err != nil {
|
2021-03-01 06:49:20 +00:00
|
|
|
return err
|
2021-02-25 18:24:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
dba := &dbAccount{
|
2021-03-19 22:01:26 +00:00
|
|
|
ID: acc.ID,
|
|
|
|
Key: acc.Key,
|
|
|
|
Contact: acc.Contact,
|
|
|
|
Status: acc.Status,
|
|
|
|
CreatedAt: clock.Now(),
|
2021-02-25 18:24:24 +00:00
|
|
|
}
|
|
|
|
|
2021-03-01 06:49:20 +00:00
|
|
|
kid, err := acme.KeyToID(dba.Key)
|
2021-02-25 18:24:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
kidB := []byte(kid)
|
|
|
|
|
|
|
|
// Set the jwkID -> acme account ID index
|
2021-03-01 06:49:20 +00:00
|
|
|
_, swapped, err := db.db.CmpAndSwap(accountByKeyIDTable, kidB, nil, []byte(acc.ID))
|
2021-02-25 18:24:24 +00:00
|
|
|
switch {
|
|
|
|
case err != nil:
|
2021-03-01 06:49:20 +00:00
|
|
|
return errors.Wrap(err, "error storing keyID to accountID index")
|
2021-02-25 18:24:24 +00:00
|
|
|
case !swapped:
|
2021-03-01 06:49:20 +00:00
|
|
|
return errors.Errorf("key-id to account-id index already exists")
|
2021-02-25 18:24:24 +00:00
|
|
|
default:
|
2021-02-28 01:05:37 +00:00
|
|
|
if err = db.save(ctx, acc.ID, dba, nil, "account", accountTable); err != nil {
|
2021-02-25 18:24:24 +00:00
|
|
|
db.db.Del(accountByKeyIDTable, kidB)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateAccount imlements the AcmeDB.UpdateAccount interface.
|
2021-03-01 06:49:20 +00:00
|
|
|
func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error {
|
2021-02-28 01:05:37 +00:00
|
|
|
old, err := db.getDBAccount(ctx, acc.ID)
|
2021-02-25 18:24:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-02-26 18:12:30 +00:00
|
|
|
nu := old.clone()
|
2021-03-01 06:49:20 +00:00
|
|
|
nu.Contact = acc.Contact
|
2021-02-26 18:12:30 +00:00
|
|
|
nu.Status = acc.Status
|
2021-02-25 18:24:24 +00:00
|
|
|
|
|
|
|
// If the status has changed to 'deactivated', then set deactivatedAt timestamp.
|
2021-03-01 06:49:20 +00:00
|
|
|
if acc.Status == acme.StatusDeactivated && old.Status != acme.StatusDeactivated {
|
2021-03-19 22:01:26 +00:00
|
|
|
nu.DeactivatedAt = clock.Now()
|
2021-02-25 18:24:24 +00:00
|
|
|
}
|
|
|
|
|
2021-03-01 06:49:20 +00:00
|
|
|
return db.save(ctx, old.ID, nu, old, "account", accountTable)
|
2021-02-25 18:24:24 +00:00
|
|
|
}
|
2021-07-17 15:35:44 +00:00
|
|
|
|
|
|
|
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
2021-10-11 21:10:16 +00:00
|
|
|
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) {
|
2021-07-17 15:35:44 +00:00
|
|
|
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{
|
2021-09-16 21:09:24 +00:00
|
|
|
ID: keyID,
|
|
|
|
Provisioner: provisionerName,
|
|
|
|
Reference: reference,
|
|
|
|
KeyBytes: random,
|
|
|
|
CreatedAt: clock.Now(),
|
2021-07-17 15:35:44 +00:00
|
|
|
}
|
|
|
|
|
2021-10-11 21:34:23 +00:00
|
|
|
if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil {
|
2021-07-17 15:35:44 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-09-17 15:08:02 +00:00
|
|
|
|
|
|
|
if dbeak.Reference != "" {
|
|
|
|
dbExternalAccountKeyReference := &dbExternalAccountKeyReference{
|
|
|
|
Reference: dbeak.Reference,
|
|
|
|
ExternalAccountKeyID: dbeak.ID,
|
|
|
|
}
|
2021-10-11 21:34:23 +00:00
|
|
|
if err := db.save(ctx, dbeak.Reference, dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil {
|
2021-09-17 15:08:02 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-17 15:35:44 +00:00
|
|
|
return &acme.ExternalAccountKey{
|
2021-09-16 21:09:24 +00:00
|
|
|
ID: dbeak.ID,
|
|
|
|
Provisioner: dbeak.Provisioner,
|
|
|
|
Reference: dbeak.Reference,
|
|
|
|
AccountID: dbeak.AccountID,
|
|
|
|
KeyBytes: dbeak.KeyBytes,
|
|
|
|
CreatedAt: dbeak.CreatedAt,
|
|
|
|
BoundAt: dbeak.BoundAt,
|
2021-07-17 15:35:44 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetExternalAccountKey retrieves an External Account Binding key by KeyID
|
2021-10-11 21:10:16 +00:00
|
|
|
func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
|
2021-07-17 17:02:47 +00:00
|
|
|
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
2021-07-17 15:35:44 +00:00
|
|
|
if err != nil {
|
2021-07-17 17:02:47 +00:00
|
|
|
return nil, err
|
2021-07-17 15:35:44 +00:00
|
|
|
}
|
|
|
|
|
2021-09-16 21:09:24 +00:00
|
|
|
if dbeak.Provisioner != provisionerName {
|
2021-08-09 08:26:31 +00:00
|
|
|
return nil, acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created")
|
|
|
|
}
|
|
|
|
|
2021-07-17 15:35:44 +00:00
|
|
|
return &acme.ExternalAccountKey{
|
2021-09-16 21:09:24 +00:00
|
|
|
ID: dbeak.ID,
|
|
|
|
Provisioner: dbeak.Provisioner,
|
|
|
|
Reference: dbeak.Reference,
|
|
|
|
AccountID: dbeak.AccountID,
|
|
|
|
KeyBytes: dbeak.KeyBytes,
|
|
|
|
CreatedAt: dbeak.CreatedAt,
|
|
|
|
BoundAt: dbeak.BoundAt,
|
2021-07-17 15:35:44 +00:00
|
|
|
}, nil
|
|
|
|
}
|
2021-07-17 17:02:47 +00:00
|
|
|
|
2021-10-11 21:10:16 +00:00
|
|
|
func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error {
|
2021-09-17 15:48:09 +00:00
|
|
|
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
2021-08-27 12:10:00 +00:00
|
|
|
if err != nil {
|
2021-08-27 12:47:10 +00:00
|
|
|
return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID)
|
|
|
|
}
|
2021-09-17 15:48:09 +00:00
|
|
|
if dbeak.Provisioner != provisionerName {
|
2021-10-08 23:02:00 +00:00
|
|
|
return errors.New("name of provisioner does not match provisioner for which the EAB key was created")
|
2021-09-17 15:48:09 +00:00
|
|
|
}
|
|
|
|
if dbeak.Reference != "" {
|
|
|
|
err = db.db.Del(externalAccountKeysByReferenceTable, []byte(dbeak.Reference))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error deleting ACME EAB Key Reference with Key ID %s and reference %s", keyID, dbeak.Reference)
|
|
|
|
}
|
|
|
|
}
|
2021-12-20 13:30:01 +00:00
|
|
|
if err = db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil {
|
2021-08-27 12:47:10 +00:00
|
|
|
return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID)
|
2021-08-27 12:10:00 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-08-27 14:58:04 +00:00
|
|
|
// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner
|
2021-10-17 20:42:36 +00:00
|
|
|
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
2021-08-27 14:58:04 +00:00
|
|
|
entries, err := db.db.List(externalAccountKeyTable)
|
|
|
|
if err != nil {
|
2021-10-17 20:42:36 +00:00
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
// set sane limits; based on the Admin API limits
|
|
|
|
switch {
|
|
|
|
case limit <= 0:
|
|
|
|
limit = 20
|
|
|
|
case limit > 100:
|
|
|
|
limit = 100
|
2021-08-27 14:58:04 +00:00
|
|
|
}
|
|
|
|
|
2021-10-17 20:42:36 +00:00
|
|
|
foundCursorKey := false
|
2021-09-16 21:09:24 +00:00
|
|
|
keys := []*acme.ExternalAccountKey{}
|
2021-10-17 20:42:36 +00:00
|
|
|
for _, entry := range entries { // entries is sorted alphabetically on the key (ID) of the EAK; no need to sort this again.
|
2021-08-27 14:58:04 +00:00
|
|
|
dbeak := new(dbExternalAccountKey)
|
|
|
|
if err = json.Unmarshal(entry.Value, dbeak); err != nil {
|
2021-10-17 20:42:36 +00:00
|
|
|
return nil, "", errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key))
|
2021-08-27 14:58:04 +00:00
|
|
|
}
|
2021-09-16 21:09:24 +00:00
|
|
|
if dbeak.Provisioner != provisionerName {
|
|
|
|
continue
|
2021-08-27 14:58:04 +00:00
|
|
|
}
|
2021-10-17 20:42:36 +00:00
|
|
|
// skip the IDs not matching the cursor to look for in the sorted list.
|
|
|
|
if cursor != "" && !foundCursorKey && cursor != dbeak.ID {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// look for the entry pointed to by the cursor (the next item to return), to start selecting items
|
|
|
|
if cursor != "" && !foundCursorKey && cursor == dbeak.ID {
|
|
|
|
foundCursorKey = true
|
|
|
|
}
|
|
|
|
// return if the limit of items was found in the previous iteration; the next cursor is set to the next item to return
|
|
|
|
if len(keys) == limit {
|
|
|
|
return keys, dbeak.ID, nil
|
|
|
|
}
|
2021-09-16 21:09:24 +00:00
|
|
|
keys = append(keys, &acme.ExternalAccountKey{
|
|
|
|
ID: dbeak.ID,
|
|
|
|
KeyBytes: dbeak.KeyBytes,
|
|
|
|
Provisioner: dbeak.Provisioner,
|
|
|
|
Reference: dbeak.Reference,
|
|
|
|
AccountID: dbeak.AccountID,
|
|
|
|
CreatedAt: dbeak.CreatedAt,
|
|
|
|
BoundAt: dbeak.BoundAt,
|
|
|
|
})
|
2021-08-27 14:58:04 +00:00
|
|
|
}
|
|
|
|
|
2021-10-17 20:42:36 +00:00
|
|
|
return keys, "", nil
|
2021-08-27 14:58:04 +00:00
|
|
|
}
|
|
|
|
|
2021-09-17 15:08:02 +00:00
|
|
|
// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference
|
2021-10-11 21:10:16 +00:00
|
|
|
func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) {
|
2021-09-17 15:25:19 +00:00
|
|
|
if reference == "" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2021-09-17 15:08:02 +00:00
|
|
|
k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(reference))
|
2021-09-17 15:48:09 +00:00
|
|
|
if nosqlDB.IsErrNotFound(err) {
|
2021-10-16 12:44:56 +00:00
|
|
|
return nil, acme.ErrNotFound
|
2021-09-17 15:08:02 +00:00
|
|
|
} 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, provisionerName, dbExternalAccountKeyReference.ExternalAccountKeyID)
|
|
|
|
}
|
|
|
|
|
2021-08-09 08:26:31 +00:00
|
|
|
func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error {
|
2021-07-17 17:02:47 +00:00
|
|
|
old, err := db.getDBExternalAccountKey(ctx, eak.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-09-16 21:09:24 +00:00
|
|
|
if old.Provisioner != provisionerName {
|
2021-10-08 23:02:00 +00:00
|
|
|
return errors.New("name of provisioner does not match provisioner for which the EAB key was created")
|
2021-08-09 08:26:31 +00:00
|
|
|
}
|
|
|
|
|
2021-07-17 17:02:47 +00:00
|
|
|
nu := dbExternalAccountKey{
|
2021-09-16 21:09:24 +00:00
|
|
|
ID: eak.ID,
|
|
|
|
Provisioner: eak.Provisioner,
|
|
|
|
Reference: eak.Reference,
|
|
|
|
AccountID: eak.AccountID,
|
|
|
|
KeyBytes: eak.KeyBytes,
|
|
|
|
CreatedAt: eak.CreatedAt,
|
|
|
|
BoundAt: eak.BoundAt,
|
2021-07-17 17:02:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable)
|
|
|
|
}
|