forked from TrueCloudLab/certificates
Beginnings of acmeDB interface
This commit is contained in:
parent
71f59de396
commit
088432150d
3 changed files with 20 additions and 311 deletions
197
acme/account.go
197
acme/account.go
|
@ -1,197 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/smallstep/nosql"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
// account represents an ACME account.
|
|
||||||
type account 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAccount returns a new acme account type.
|
|
||||||
func newAccount(db nosql.DB, ops AccountOptions) (*account, error) {
|
|
||||||
id, err := randID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
a := &account{
|
|
||||||
ID: id,
|
|
||||||
Key: ops.Key,
|
|
||||||
Contact: ops.Contact,
|
|
||||||
Status: "valid",
|
|
||||||
Created: clock.Now(),
|
|
||||||
}
|
|
||||||
return a, a.saveNew(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// save writes the Account to the DB.
|
|
||||||
// If the account is new then the necessary indices will be created.
|
|
||||||
// Else, the account in the DB will be updated.
|
|
||||||
func (a *account) saveNew(db nosql.DB) error {
|
|
||||||
kid, err := keyToID(a.Key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
kidB := []byte(kid)
|
|
||||||
|
|
||||||
// Set the jwkID -> acme account ID index
|
|
||||||
_, swapped, err := 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 = a.save(db, nil); err != nil {
|
|
||||||
db.Del(accountByKeyIDTable, kidB)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *account) save(db nosql.DB, old *account) 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(*a)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error marshaling new account object")
|
|
||||||
}
|
|
||||||
// Set the Account
|
|
||||||
_, swapped, err := db.CmpAndSwap(accountTable, []byte(a.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update updates the acme account object stored in the database if,
|
|
||||||
// and only if, the account has not changed since the last read.
|
|
||||||
func (a *account) update(db nosql.DB, contact []string) (*account, error) {
|
|
||||||
b := *a
|
|
||||||
b.Contact = contact
|
|
||||||
if err := b.save(db, a); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deactivate deactivates the acme account.
|
|
||||||
func (a *account) deactivate(db nosql.DB) (*account, error) {
|
|
||||||
b := *a
|
|
||||||
b.Status = StatusDeactivated
|
|
||||||
b.Deactivated = clock.Now()
|
|
||||||
if err := b.save(db, a); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAccountByID retrieves the account with the given ID.
|
|
||||||
func getAccountByID(db nosql.DB, id string) (*account, error) {
|
|
||||||
ab, err := db.Get(accountTable, []byte(id))
|
|
||||||
if err != nil {
|
|
||||||
if nosql.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 getAccountByKeyID(db nosql.DB, kid string) (*account, error) {
|
|
||||||
id, err := db.Get(accountByKeyIDTable, []byte(kid))
|
|
||||||
if err != nil {
|
|
||||||
if nosql.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))
|
|
||||||
}
|
|
|
@ -92,7 +92,7 @@ func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority) (*Aut
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Autohrity that implements the ACME interface.
|
// New returns a new Authority that implements the ACME interface.
|
||||||
func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
|
func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
|
||||||
if _, ok := ops.DB.(*database.SimpleDB); !ok {
|
if _, ok := ops.DB.(*database.SimpleDB); !ok {
|
||||||
// If it's not a SimpleDB then go ahead and bootstrap the DB with the
|
// If it's not a SimpleDB then go ahead and bootstrap the DB with the
|
||||||
|
@ -140,59 +140,41 @@ func (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNonce generates, stores, and returns a new ACME nonce.
|
// NewNonce generates, stores, and returns a new ACME nonce.
|
||||||
func (a *Authority) NewNonce() (string, error) {
|
func (a *Authority) NewNonce(ctx context.Context) (string, error) {
|
||||||
n, err := newNonce(a.db)
|
return a.db.CreateNonce(ctx)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return n.ID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseNonce consumes the given nonce if it is valid, returns error otherwise.
|
// UseNonce consumes the given nonce if it is valid, returns error otherwise.
|
||||||
func (a *Authority) UseNonce(nonce string) error {
|
func (a *Authority) UseNonce(ctx context.Context, nonce string) error {
|
||||||
return useNonce(a.db, nonce)
|
return a.db.DeleteNonce(ctx, nonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAccount creates, stores, and returns a new ACME account.
|
// NewAccount creates, stores, and returns a new ACME account.
|
||||||
func (a *Authority) NewAccount(ctx context.Context, ao AccountOptions) (*Account, error) {
|
func (a *Authority) NewAccount(ctx context.Context, ao AccountOptions) (*Account, error) {
|
||||||
acc, err := newAccount(a.db, ao)
|
a := NewAccount(ao)
|
||||||
if err != nil {
|
if err := a.db.CreateAccount(ctx, a); err != nil {
|
||||||
return nil, err
|
return ServerInternalErr(err)
|
||||||
}
|
}
|
||||||
return acc.toACME(ctx, a.db, a.dir)
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAccount updates an ACME account.
|
// UpdateAccount updates an ACME account.
|
||||||
func (a *Authority) UpdateAccount(ctx context.Context, id string, contact []string) (*Account, error) {
|
func (a *Authority) UpdateAccount(ctx context.Context, auo AccountUpdateOptions) (*Account, error) {
|
||||||
acc, err := getAccountByID(a.db, id)
|
acc, err := a.db.GetAccount(ctx, auo.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ServerInternalErr(err)
|
return ServerInternalErr(err)
|
||||||
}
|
}
|
||||||
if acc, err = acc.update(a.db, contact); err != nil {
|
acc.Contact = auo.Contact
|
||||||
return nil, err
|
acc.Status = auo.Status
|
||||||
|
if err = a.db.UpdateAccount(ctx, acc); err != nil {
|
||||||
|
return ServerInternalErr(err)
|
||||||
}
|
}
|
||||||
return acc.toACME(ctx, a.db, a.dir)
|
return acc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount returns an ACME account.
|
// GetAccount returns an ACME account.
|
||||||
func (a *Authority) GetAccount(ctx context.Context, id string) (*Account, error) {
|
func (a *Authority) GetAccount(ctx context.Context, id string) (*Account, error) {
|
||||||
acc, err := getAccountByID(a.db, id)
|
return a.db.GetAccount(ctx, id)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return acc.toACME(ctx, a.db, a.dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeactivateAccount deactivates an ACME account.
|
|
||||||
func (a *Authority) DeactivateAccount(ctx context.Context, id string) (*Account, error) {
|
|
||||||
acc, err := getAccountByID(a.db, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if acc, err = acc.deactivate(a.db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return acc.toACME(ctx, a.db, a.dir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyToID(jwk *jose.JSONWebKey) (string, error) {
|
func keyToID(jwk *jose.JSONWebKey) (string, error) {
|
||||||
|
@ -209,11 +191,8 @@ func (a *Authority) GetAccountByKey(ctx context.Context, jwk *jose.JSONWebKey) (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
acc, err := getAccountByKeyID(a.db, kid)
|
acc, err := a.db.GetAccountByKeyID(ctx, kid)
|
||||||
if err != nil {
|
return acc, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return acc.toACME(ctx, a.db, a.dir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrder returns an ACME order.
|
// GetOrder returns an ACME order.
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/smallstep/nosql"
|
|
||||||
"github.com/smallstep/nosql/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nonce contains nonce metadata used in the ACME protocol.
|
|
||||||
type nonce struct {
|
|
||||||
ID string
|
|
||||||
Created time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// newNonce creates, stores, and returns an ACME replay-nonce.
|
|
||||||
func newNonce(db nosql.DB) (*nonce, error) {
|
|
||||||
_id, err := randID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id := base64.RawURLEncoding.EncodeToString([]byte(_id))
|
|
||||||
n := &nonce{
|
|
||||||
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 n, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// useNonce 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 useNonce(db nosql.DB, nonce string) error {
|
|
||||||
err := 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 nosql.IsErrNotFound(err):
|
|
||||||
return BadNonceErr(nil)
|
|
||||||
case err != nil:
|
|
||||||
return ServerInternalErr(errors.Wrapf(err, "error deleting nonce %s", nonce))
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue