[acme db interface] wip

This commit is contained in:
max furman 2021-02-28 10:09:06 -08:00
parent 461bad3fef
commit 121cc34cca
10 changed files with 190 additions and 151 deletions

View file

@ -38,29 +38,5 @@ func (a *Account) GetKey() *jose.JSONWebKey {
// IsValid returns true if the Account is valid.
func (a *Account) IsValid() bool {
return a.Status == StatusValid
return Status(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
//}

View file

@ -28,16 +28,16 @@ type Interface interface {
DeactivateAccount(ctx context.Context, accID string) (*Account, error)
GetAccount(ctx context.Context, accID string) (*Account, error)
GetAccountByKey(ctx context.Context, key *jose.JSONWebKey) (*Account, error)
NewAccount(ctx context.Context, ao AccountOptions) (*Account, error)
UpdateAccount(context.Context, string, []string) (*Account, error)
NewAccount(ctx context.Context, acc *Account) (*Account, error)
UpdateAccount(ctx context.Context, acc *Account) (*Account, error)
GetAuthz(ctx context.Context, accID string, authzID string) (*Authz, error)
GetAuthz(ctx context.Context, accID string, authzID string) (*Authorization, error)
ValidateChallenge(ctx context.Context, accID string, chID string, key *jose.JSONWebKey) (*Challenge, error)
FinalizeOrder(ctx context.Context, accID string, orderID string, csr *x509.CertificateRequest) (*Order, error)
GetOrder(ctx context.Context, accID string, orderID string) (*Order, error)
GetOrdersByAccount(ctx context.Context, accID string) ([]string, error)
NewOrder(ctx context.Context, oo OrderOptions) (*Order, error)
NewOrder(ctx context.Context, o *Order) (*Order, error)
GetCertificate(string, string) ([]byte, error)
@ -140,22 +140,19 @@ func (a *Authority) UseNonce(ctx context.Context, nonce string) error {
}
// NewAccount creates, stores, and returns a new ACME account.
func (a *Authority) NewAccount(ctx context.Context, ao AccountOptions) (*Account, error) {
a := NewAccount(ao)
if err := a.db.CreateAccount(ctx, a); err != nil {
func (a *Authority) NewAccount(ctx context.Context, acc *Account) (*Account, error) {
if err := a.db.CreateAccount(ctx, acc); err != nil {
return ServerInternalErr(err)
}
return a, nil
}
// UpdateAccount updates an ACME account.
func (a *Authority) UpdateAccount(ctx context.Context, auo AccountUpdateOptions) (*Account, error) {
acc, err := a.db.GetAccount(ctx, auo.ID)
if err != nil {
return ServerInternalErr(err)
}
acc.Contact = auo.Contact
acc.Status = auo.Status
func (a *Authority) UpdateAccount(ctx context.Context, acc *Account) (*Account, error) {
/*
acc.Contact = auo.Contact
acc.Status = auo.Status
*/
if err = a.db.UpdateAccount(ctx, acc); err != nil {
return ServerInternalErr(err)
}
@ -228,20 +225,19 @@ func (a *Authority) GetOrdersByAccount(ctx context.Context, id string) ([]string
}
// NewOrder generates, stores, and returns a new ACME order.
func (a *Authority) NewOrder(ctx context.Context, ops OrderOptions) (*Order, error) {
func (a *Authority) NewOrder(ctx context.Context, o *Order) (*Order, error) {
prov, err := ProvisionerFromContext(ctx)
if err != nil {
return nil, err
}
return db.CreateOrder(ctx, &Order{
AccountID: ops.AccountID,
ProvisionerID: prov.GetID(),
Backdate: a.backdate.Duration,
DefaultDuration: prov.DefaultTLSCertDuration(),
Identifiers: ops.Identifiers,
NotBefore: ops.NotBefore,
NotAfter: ops.NotAfter,
})
o.DefaultDuration = prov.DefaultTLSCertDuration()
o.Backdate = a.backdate.Duration
o.ProvisionerID = prov.GetID()
if err = db.CreateOrder(ctx, o); err != nil {
return nil, ServerInternalErr(err)
}
return o, nil
}
// FinalizeOrder attempts to finalize an order and generate a new certificate.
@ -271,7 +267,7 @@ func (a *Authority) FinalizeOrder(ctx context.Context, accID, orderID string, cs
// GetAuthz retrieves and attempts to update the status on an ACME authz
// before returning.
func (a *Authority) GetAuthz(ctx context.Context, accID, authzID string) (*Authz, error) {
func (a *Authority) GetAuthz(ctx context.Context, accID, authzID string) (*Authorization, error) {
az, err := a.db.GetAuthorization(ctx, authzID)
if err != nil {
return nil, err
@ -316,13 +312,14 @@ func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, j
}
// GetCertificate retrieves the Certificate by ID.
func (a *Authority) GetCertificate(accID, certID string) ([]byte, error) {
cert, err := getCert(a.db, certID)
func (a *Authority) GetCertificate(ctx context.Context, accID, certID string) ([]byte, error) {
cert, err := a.db.GetCertificate(a.db, certID)
if err != nil {
return nil, err
}
if accID != cert.AccountID {
return nil, UnauthorizedErr(errors.New("account does not own certificate"))
if cert.AccountID != accID {
log.Printf("account-id from request ('%s') does not match challenge account-id ('%s')", accID, cert.AccountID)
return nil, UnauthorizedErr(errors.New("account does not own challenge"))
}
return cert.toACME(a.db, a.dir)
}

View file

@ -1,4 +1,4 @@
package types
package acme
import (
"context"

View file

@ -2,88 +2,28 @@ package acme
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"time"
"github.com/pkg/errors"
"github.com/smallstep/nosql"
)
type certificate struct {
ID string `json:"id"`
Created time.Time `json:"created"`
AccountID string `json:"accountID"`
OrderID string `json:"orderID"`
Leaf []byte `json:"leaf"`
Intermediates []byte `json:"intermediates"`
}
// CertOptions options with which to create and store a cert object.
type CertOptions struct {
// Certificate options with which to create and store a cert object.
type Certificate struct {
ID string
AccountID string
OrderID string
Leaf *x509.Certificate
Intermediates []*x509.Certificate
}
func newCert(db nosql.DB, ops CertOptions) (*certificate, error) {
id, err := randID()
if err != nil {
return nil, err
}
leaf := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ops.Leaf.Raw,
})
var intermediates []byte
for _, cert := range ops.Intermediates {
intermediates = append(intermediates, pem.EncodeToMemory(&pem.Block{
// ToACME encodes the entire X509 chain into a PEM list.
func (cert *Certificate) ToACME(db nosql.DB, dir *directory) ([]byte, error) {
var ret []byte
for _, c := range append([]*x509.Certificate{cert.Leaf}, cert.Intermediates...) {
ret = append(ret, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
Bytes: c.Raw,
})...)
}
cert := &certificate{
ID: id,
AccountID: ops.AccountID,
OrderID: ops.OrderID,
Leaf: leaf,
Intermediates: intermediates,
Created: time.Now().UTC(),
}
certB, err := json.Marshal(cert)
if err != nil {
return nil, ServerInternalErr(errors.Wrap(err, "error marshaling certificate"))
}
_, swapped, err := db.CmpAndSwap(certTable, []byte(id), nil, certB)
switch {
case err != nil:
return nil, ServerInternalErr(errors.Wrap(err, "error storing certificate"))
case !swapped:
return nil, ServerInternalErr(errors.New("error storing certificate; " +
"value has changed since last read"))
default:
return cert, nil
}
}
func (c *certificate) toACME(db nosql.DB, dir *directory) ([]byte, error) {
return append(c.Leaf, c.Intermediates...), nil
}
func getCert(db nosql.DB, id string) (*certificate, error) {
b, err := db.Get(certTable, []byte(id))
if nosql.IsErrNotFound(err) {
return nil, MalformedErr(errors.Wrapf(err, "certificate %s not found", id))
} else if err != nil {
return nil, ServerInternalErr(errors.Wrap(err, "error loading certificate"))
}
var cert certificate
if err := json.Unmarshal(b, &cert); err != nil {
return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling certificate"))
}
return &cert, nil
return ret, nil
}

View file

@ -1,4 +1,4 @@
package types
package acme
import (
"context"

View file

@ -4,28 +4,28 @@ import "context"
// DB is the DB interface expected by the step-ca ACME API.
type DB interface {
CreateAccount(ctx context.Context, acc *types.Account) (*types.Account, error)
GetAccount(ctx context.Context, id string) (*types.Account, error)
GetAccountByKeyID(ctx context.Context, kid string) (*types.Account, error)
UpdateAccount(ctx context.Context, acc *types.Account) error
CreateAccount(ctx context.Context, acc *Account) (*Account, error)
GetAccount(ctx context.Context, id string) (*Account, error)
GetAccountByKeyID(ctx context.Context, kid string) (*Account, error)
UpdateAccount(ctx context.Context, acc *Account) error
CreateNonce(ctx context.Context) (types.Nonce, error)
DeleteNonce(ctx context.Context, nonce types.Nonce) error
CreateNonce(ctx context.Context) (Nonce, error)
DeleteNonce(ctx context.Context, nonce Nonce) error
CreateAuthorization(ctx context.Context, authz *types.Authorization) error
GetAuthorization(ctx context.Context, id string) (*types.Authorization, error)
UpdateAuthorization(ctx context.Context, authz *types.Authorization) error
CreateAuthorization(ctx context.Context, az *Authorization) error
GetAuthorization(ctx context.Context, id string) (*Authorization, error)
UpdateAuthorization(ctx context.Context, az *Authorization) error
CreateCertificate(ctx context.Context, cert *types.Certificate) error
GetCertificate(ctx context.Context, id string) (*types.Certificate, error)
CreateCertificate(ctx context.Context, cert *Certificate) error
GetCertificate(ctx context.Context, id string) (*Certificate, error)
CreateChallenge(ctx context.Context, ch *types.Challenge) error
GetChallenge(ctx context.Context, id, authzID string) (*types.Challenge, error)
UpdateChallenge(ctx context.Context, ch *types.Challenge) error
CreateChallenge(ctx context.Context, ch *Challenge) error
GetChallenge(ctx context.Context, id, authzID string) (*Challenge, error)
UpdateChallenge(ctx context.Context, ch *Challenge) error
CreateOrder(ctx context.Context, o *types.Order) error
CreateOrder(ctx context.Context, o *Order) error
DeleteOrder(ctx context.Context, id string) error
GetOrder(ctx context.Context, id string) (*types.Order, error)
GetOrder(ctx context.Context, id string) (*Order, error)
GetOrdersByAccountID(ctx context.Context, accountID string) error
UpdateOrder(ctx context.Context, o *types.Order) error
UpdateOrder(ctx context.Context, o *Order) error
}

View file

@ -26,7 +26,7 @@ func (dba *dbAccount) clone() *dbAccount {
}
// CreateAccount imlements the AcmeDB.CreateAccount interface.
func (db *DB) CreateAccount(ctx context.Context, acc *types.Account) error {
func (db *DB) CreateAccount(ctx context.Context, acc *Account) error {
acc.ID, err = randID()
if err != nil {
return nil, err
@ -63,9 +63,13 @@ func (db *DB) CreateAccount(ctx context.Context, acc *types.Account) error {
}
// GetAccount retrieves an ACME account by ID.
func (db *DB) GetAccount(ctx context.Context, id string) (*types.Account, error) {
func (db *DB) GetAccount(ctx context.Context, id string) (*Account, error) {
acc, err := db.getDBAccount(ctx, id)
if err != nil {
return nil, err
}
return &types.Account{
return &Account{
Status: dbacc.Status,
Contact: dbacc.Contact,
Orders: dir.getLink(ctx, OrdersByAccountLink, true, a.ID),
@ -75,7 +79,7 @@ func (db *DB) GetAccount(ctx context.Context, id string) (*types.Account, error)
}
// GetAccountByKeyID retrieves an ACME account by KeyID (thumbprint of the Account Key -- JWK).
func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*types.Account, error) {
func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) {
id, err := db.getAccountIDByKeyID(kid)
if err != nil {
return nil, err
@ -84,7 +88,7 @@ func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*types.Account
}
// UpdateAccount imlements the AcmeDB.UpdateAccount interface.
func (db *DB) UpdateAccount(ctx context.Context, acc *types.Account) error {
func (db *DB) UpdateAccount(ctx context.Context, acc *Account) error {
if len(acc.ID) == 0 {
return ServerInternalErr(errors.New("id cannot be empty"))
}
@ -99,7 +103,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *types.Account) error {
nu.Status = acc.Status
// If the status has changed to 'deactivated', then set deactivatedAt timestamp.
if acc.Status == types.StatusDeactivated && old.Status != types.Status.Deactivated {
if acc.Status == StatusDeactivated && old.Status != Status.Deactivated {
nu.Deactivated = clock.Now()
}

View file

@ -0,0 +1,121 @@
package nosql
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"time"
"github.com/pkg/errors"
"github.com/smallstep/nosql"
)
type dbCert struct {
ID string `json:"id"`
Created time.Time `json:"created"`
AccountID string `json:"accountID"`
OrderID string `json:"orderID"`
Leaf []byte `json:"leaf"`
Intermediates []byte `json:"intermediates"`
}
// CreateCertificate creates and stores an ACME certificate type.
func (db *DB) CreateCertificate(ctx context.Context, cert *Certificate) error {
cert.id, err = randID()
if err != nil {
return err
}
leaf := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ops.Leaf.Raw,
})
var intermediates []byte
for _, cert := range ops.Intermediates {
intermediates = append(intermediates, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})...)
}
cert := &dbCert{
ID: cert.ID,
AccountID: cert.AccountID,
OrderID: cert.OrderID,
Leaf: leaf,
Intermediates: intermediates,
Created: time.Now().UTC(),
}
return db.save(ctx, cert.ID, cert, nil, "certificate", certTable)
}
// GetCertificate retrieves and unmarshals an ACME certificate type from the
// datastore.
func (db *DB) GetCertificate(ctx context.Context, id string) (*Certificate, error) {
b, err := db.db.Get(certTable, []byte(id))
if nosql.IsErrNotFound(err) {
return nil, MalformedErr(errors.Wrapf(err, "certificate %s not found", id))
} else if err != nil {
return nil, ServerInternalErr(errors.Wrap(err, "error loading certificate"))
}
var dbCert certificate
if err := json.Unmarshal(b, &dbCert); err != nil {
return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling certificate"))
}
leaf, err := parseCert(dbCert.Leaf)
if err != nil {
return nil, ServerInternalErr(errors.Wrapf("error parsing leaf of ACME Certificate with ID '%s'", id))
}
intermediates, err := parseBundle(dbCert.Intermediates)
if err != nil {
return nil, ServerInternalErr(errors.Wrapf("error parsing intermediate bundle of ACME Certificate with ID '%s'", id))
}
return &Certificate{
ID: dbCert.ID,
AccountID: dbCert.AccountID,
OrderID: dbCert.OrderID,
Leaf: leaf,
Intermediates: intermediate,
}
}
func parseCert(b []byte) (*x509.Certificate, error) {
block, rest := pem.Decode(dbCert.Leaf)
if block == nil || len(rest) > 0 {
return nil, errors.New("error decoding PEM block: contains unexpected data")
}
if block.Type != "CERTIFICATE" {
return nil, errors.New("error decoding PEM: block is not a certificate bundle")
}
var crt *x509.Certificate
crt, err = x509.ParseCertificate(block.Bytes)
}
func parseBundle(b []byte) ([]*x509.Certificate, error) {
var block *pem.Block
var bundle []*x509.Certificate
for len(b) > 0 {
block, b = pem.Decode(b)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
return nil, errors.Errorf("error decoding PEM: file '%s' is not a certificate bundle", filename)
}
var crt *x509.Certificate
crt, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrapf(err, "error parsing %s", filename)
}
bundle = append(bundle, crt)
}
if len(b) > 0 {
return nil, errors.Errorf("error decoding PEM: file '%s' contains unexpected data", filename)
}
return bundle, nil
}

View file

@ -1,3 +1,4 @@
package acme
import (
"context"
@ -188,7 +189,7 @@ func (o *order) Finalize(ctx, db DB, csr *x509.CertificateRequest, auth SignAuth
return nil, ServerInternalErr(errors.Wrapf(err, "error generating certificate for order %s", o.ID))
}
cert, err := newCert(db, CertOptions{
cert, err := db.CreateCertificate(ctx, &Certificate{
AccountID: o.AccountID,
OrderID: o.ID,
Leaf: certChain[0],

View file

@ -1,4 +1,4 @@
package types
package acme
// Status represents an ACME status.
type Status string