forked from TrueCloudLab/certificates
[acme db interface] wip
This commit is contained in:
parent
461bad3fef
commit
121cc34cca
10 changed files with 190 additions and 151 deletions
|
@ -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
|
||||
//}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package types
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -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{
|
||||
// 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: ops.Leaf.Raw,
|
||||
})
|
||||
var intermediates []byte
|
||||
for _, cert := range ops.Intermediates {
|
||||
intermediates = append(intermediates, 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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package types
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
34
acme/db.go
34
acme/db.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package types
|
||||
package acme
|
||||
|
||||
// Status represents an ACME status.
|
||||
type Status string
|
||||
|
|
Loading…
Reference in a new issue