diff --git a/acme/db/db.go b/acme/db/db.go new file mode 100644 index 00000000..2882fcf4 --- /dev/null +++ b/acme/db/db.go @@ -0,0 +1,31 @@ +package acme + +import "context" + +// DB is the DB interface expected by the step-ca ACME API. +type DB interface { + CreateAccount(ctx context.Context, acc *Account) (*Account, error) + GetAccount(ctx context.Context, id string) (*Account, error) + GetAccountByKeyID(ctx context.Context) (*Account, error) + UpdateAccount(ctx context.Context, acc *Account) error + + CreateNonce(ctx context.Context) (Nonce, error) + DeleteNonce(ctx context.Context, nonce Nonce) error + + CreateAuthorization(ctx context.Context, authz *Authorization) error + GetAuthorization(ctx context.Context, id string) (*Authorization, error) + UpdateAuthorization(ctx context.Context, authz *Authorization) error + + CreateCertificate(ctx context.Context, cert *Certificate) error + GetCertificate(ctx context.Context, id string) (*Certificate, error) + + CreateChallenge(ctx context.Context, ch *Challenge) error + GetChallenge(ctx context.Context, id string) (*Challenge, error) + UpdateChallenge(ctx context.Context, ch *Challenge) error + + CreateOrder(ctx context.Context, o *Order) error + DeleteOrder(ctx context.Context, id string) error + GetOrder(ctx context.Context, id string) (*Order, error) + GetOrdersByAccountID(ctx context.Context, accountID string) error + UpdateOrder(ctx context.Context, o *Order) error +} diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go new file mode 100644 index 00000000..dcbfd2f5 --- /dev/null +++ b/acme/db/nosql/account.go @@ -0,0 +1,138 @@ +package nosql + +import ( + "context" + "encoding/json" + "time" + + "github.com/pkg/errors" + nosqlDB "github.com/smallstep/nosql" + "go.step.sm/crypto/jose" +) + +// dbAccount represents an ACME account. +type dbAccount struct { + ID string `json:"id"` + Created time.Time `json:"created"` + Deactivated time.Time `json:"deactivated"` + Key *jose.JSONWebKey `json:"key"` + Contact []string `json:"contact,omitempty"` + Status string `json:"status"` +} + +func (db *DB) saveAccount(nu *dbAccount, old *dbAccount) error { + var ( + err error + oldB []byte + ) + if old == nil { + oldB = nil + } else { + if oldB, err = json.Marshal(old); err != nil { + return ServerInternalErr(errors.Wrap(err, "error marshaling old acme order")) + } + } + + b, err := json.Marshal(*nu) + if err != nil { + return errors.Wrap(err, "error marshaling new account object") + } + // Set the Account + _, swapped, err := db.CmpAndSwap(accountTable, []byte(nu.ID), oldB, b) + switch { + case err != nil: + return ServerInternalErr(errors.Wrap(err, "error storing account")) + case !swapped: + return ServerInternalErr(errors.New("error storing account; " + + "value has changed since last read")) + default: + return nil + } +} + +// CreateAccount imlements the AcmeDB.CreateAccount interface. +func (db *DB) CreateAccount(ctx context.Context, acc *Account) error { + id, err := randID() + if err != nil { + return nil, err + } + + dba := &dbAccount{ + ID: id, + Key: acc.Key, + Contact: acc.Contact, + Status: acc.Valid, + Created: clock.Now(), + } + + kid, err := keyToID(dba.Key) + if err != nil { + return err + } + kidB := []byte(kid) + + // Set the jwkID -> acme account ID index + _, swapped, err := db.db.CmpAndSwap(accountByKeyIDTable, kidB, nil, []byte(a.ID)) + switch { + case err != nil: + return ServerInternalErr(errors.Wrap(err, "error setting key-id to account-id index")) + case !swapped: + return ServerInternalErr(errors.Errorf("key-id to account-id index already exists")) + default: + if err = db.saveAccount(dba, nil); err != nil { + db.db.Del(accountByKeyIDTable, kidB) + return err + } + return nil + } +} + +// UpdateAccount imlements the AcmeDB.UpdateAccount interface. +func (db *DB) UpdateAccount(ctx context.Context, acc *Account) error { + kid, err := keyToID(dba.Key) + if err != nil { + return err + } + + dba, err := db.db.getAccountByKeyID(ctx, kid) + + newdba := *dba + newdba.Contact = acc.contact + newdba.Status = acc.Status + + // If the status has changed to 'deactivated', then set deactivatedAt timestamp. + if acc.Status == types.StatusDeactivated && dba.Status != types.Status.Deactivated { + newdba.Deactivated = clock.Now() + } + + return db.saveAccount(newdba, dba) +} + +// getAccountByID retrieves the account with the given ID. +func (db *DB) getAccountByID(ctx context.Context, id string) (*dbAccount, error) { + ab, err := db.db.Get(accountTable, []byte(id)) + if err != nil { + if nosqlDB.IsErrNotFound(err) { + return nil, MalformedErr(errors.Wrapf(err, "account %s not found", id)) + } + return nil, ServerInternalErr(errors.Wrapf(err, "error loading account %s", id)) + } + + a := new(account) + if err = json.Unmarshal(ab, a); err != nil { + return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling account")) + } + return a, nil +} + +// getAccountByKeyID retrieves Id associated with the given Kid. +func (db *DB) getAccountByKeyID(ctx context.Context, kid string) (*dbAccount, error) { + id, err := db.db.Get(accountByKeyIDTable, []byte(kid)) + if err != nil { + if nosqlDB.IsErrNotFound(err) { + return nil, MalformedErr(errors.Wrapf(err, "account with key id %s not found", kid)) + } + return nil, ServerInternalErr(errors.Wrapf(err, "error loading key-account index")) + } + return getAccountByID(db, string(id)) +} diff --git a/acme/db/nosql/authz.go b/acme/db/nosql/authz.go new file mode 100644 index 00000000..e69de29b diff --git a/acme/db/nosql/certificate.go b/acme/db/nosql/certificate.go new file mode 100644 index 00000000..e69de29b diff --git a/acme/db/nosql/challenge.go b/acme/db/nosql/challenge.go new file mode 100644 index 00000000..e69de29b diff --git a/acme/db/nosql/nonce.go b/acme/db/nosql/nonce.go new file mode 100644 index 00000000..3459f212 --- /dev/null +++ b/acme/db/nosql/nonce.go @@ -0,0 +1,74 @@ +package nosql + +import ( + "encoding/base64" + "encoding/json" + "time" + + "github.com/pkg/errors" + nosqlDB "github.com/smallstep/nosql" + "github.com/smallstep/nosql/database" +) + +// dbNonce contains nonce metadata used in the ACME protocol. +type dbNonce struct { + ID string + Created time.Time +} + +// CreateNonce creates, stores, and returns an ACME replay-nonce. +// Implements the acme.DB interface. +func (db *DB) CreateNonce() (Nonce, error) { + _id, err := randID() + if err != nil { + return nil, err + } + + id := base64.RawURLEncoding.EncodeToString([]byte(_id)) + n := &dbNonce{ + ID: id, + Created: clock.Now(), + } + b, err := json.Marshal(n) + if err != nil { + return nil, ServerInternalErr(errors.Wrap(err, "error marshaling nonce")) + } + _, swapped, err := db.CmpAndSwap(nonceTable, []byte(id), nil, b) + switch { + case err != nil: + return nil, ServerInternalErr(errors.Wrap(err, "error storing nonce")) + case !swapped: + return nil, ServerInternalErr(errors.New("error storing nonce; " + + "value has changed since last read")) + default: + return Nonce(id), nil + } +} + +// DeleteNonce verifies that the nonce is valid (by checking if it exists), +// and if so, consumes the nonce resource by deleting it from the database. +func (db *DB) DeleteNonce(nonce string) error { + err := db.db.Update(&database.Tx{ + Operations: []*database.TxEntry{ + { + Bucket: nonceTable, + Key: []byte(nonce), + Cmd: database.Get, + }, + { + Bucket: nonceTable, + Key: []byte(nonce), + Cmd: database.Delete, + }, + }, + }) + + switch { + case nosqlDB.IsErrNotFound(err): + return BadNonceErr(nil) + case err != nil: + return ServerInternalErr(errors.Wrapf(err, "error deleting nonce %s", nonce)) + default: + return nil + } +} diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go new file mode 100644 index 00000000..8bfd1a66 --- /dev/null +++ b/acme/db/nosql/nosql.go @@ -0,0 +1,10 @@ +package nosql + +import ( + nosqlDB "github.com/smallstep/nosql" +) + +// DB is a struct that implements the AcmeDB interface. +type DB struct { + db nosqlDB.DB +} diff --git a/acme/db/nosql/order.go b/acme/db/nosql/order.go new file mode 100644 index 00000000..e69de29b diff --git a/acme/types/account.go b/acme/types/account.go new file mode 100644 index 00000000..ea40a646 --- /dev/null +++ b/acme/types/account.go @@ -0,0 +1,66 @@ +package acme + +import ( + "encoding/json" + + "github.com/pkg/errors" + "go.step.sm/crypto/jose" +) + +// Account is a subset of the internal account type containing only those +// attributes required for responses in the ACME protocol. +type Account struct { + Contact []string `json:"contact,omitempty"` + Status string `json:"status"` + Orders string `json:"orders"` + ID string `json:"-"` + Key *jose.JSONWebKey `json:"-"` +} + +// ToLog enables response logging. +func (a *Account) ToLog() (interface{}, error) { + b, err := json.Marshal(a) + if err != nil { + return nil, ServerInternalErr(errors.Wrap(err, "error marshaling account for logging")) + } + return string(b), nil +} + +// GetID returns the account ID. +func (a *Account) GetID() string { + return a.ID +} + +// GetKey returns the JWK associated with the account. +func (a *Account) GetKey() *jose.JSONWebKey { + return a.Key +} + +// IsValid returns true if the Account is valid. +func (a *Account) IsValid() bool { + return a.Status == StatusValid +} + +// AccountOptions are the options needed to create a new ACME account. +type AccountOptions struct { + Key *jose.JSONWebKey + Contact []string +} + +// AccountUpdateOptions are the options needed to update an existing ACME account. +type AccountUpdateOptions struct { + Contact []string + Status types.Status +} + +// toACME converts the internal Account type into the public acmeAccount +// type for presentation in the ACME protocol. +//func (a *account) toACME(ctx context.Context, db nosql.DB, dir *directory) (*Account, error) { +// return &Account{ +// Status: a.Status, +// Contact: a.Contact, +// Orders: dir.getLink(ctx, OrdersByAccountLink, true, a.ID), +// Key: a.Key, +// ID: a.ID, +// }, nil +//} diff --git a/acme/types/authz.go b/acme/types/authz.go new file mode 100644 index 00000000..3e3a5aa7 --- /dev/null +++ b/acme/types/authz.go @@ -0,0 +1,32 @@ +package types + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// Authz is a subset of the Authz type containing only those attributes +// required for responses in the ACME protocol. +type Authz struct { + Identifier Identifier `json:"identifier"` + Status string `json:"status"` + Expires string `json:"expires"` + Challenges []*Challenge `json:"challenges"` + Wildcard bool `json:"wildcard"` + ID string `json:"-"` +} + +// ToLog enables response logging. +func (a *Authz) ToLog() (interface{}, error) { + b, err := json.Marshal(a) + if err != nil { + return nil, ServerInternalErr(errors.Wrap(err, "error marshaling authz for logging")) + } + return string(b), nil +} + +// GetID returns the Authz ID. +func (a *Authz) GetID() string { + return a.ID +} diff --git a/acme/types/nonce.go b/acme/types/nonce.go new file mode 100644 index 00000000..4234e818 --- /dev/null +++ b/acme/types/nonce.go @@ -0,0 +1,3 @@ +package acme + +type Nonce string