From 121cc34cca32a1e7afe349abc7935b5ea62d9f00 Mon Sep 17 00:00:00 2001 From: max furman Date: Sun, 28 Feb 2021 10:09:06 -0800 Subject: [PATCH] [acme db interface] wip --- acme/account.go | 26 +------- acme/authority.go | 55 ++++++++-------- acme/authorization.go | 2 +- acme/certificate.go | 80 +++-------------------- acme/challenge.go | 2 +- acme/db.go | 34 +++++----- acme/db/nosql/account.go | 16 +++-- acme/db/nosql/certificate.go | 121 +++++++++++++++++++++++++++++++++++ acme/order.go | 3 +- acme/status.go | 2 +- 10 files changed, 190 insertions(+), 151 deletions(-) diff --git a/acme/account.go b/acme/account.go index ea40a646..a0f88d49 100644 --- a/acme/account.go +++ b/acme/account.go @@ -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 -//} diff --git a/acme/authority.go b/acme/authority.go index c0b6a732..d07f591a 100644 --- a/acme/authority.go +++ b/acme/authority.go @@ -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) } diff --git a/acme/authorization.go b/acme/authorization.go index a41950cd..f1ef0adc 100644 --- a/acme/authorization.go +++ b/acme/authorization.go @@ -1,4 +1,4 @@ -package types +package acme import ( "context" diff --git a/acme/certificate.go b/acme/certificate.go index 6a31c880..f088d93c 100644 --- a/acme/certificate.go +++ b/acme/certificate.go @@ -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 } diff --git a/acme/challenge.go b/acme/challenge.go index de178d6c..e7abaf64 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -1,4 +1,4 @@ -package types +package acme import ( "context" diff --git a/acme/db.go b/acme/db.go index 19449d76..846eed04 100644 --- a/acme/db.go +++ b/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 } diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 6e9ee8c0..e863c371 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -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() } diff --git a/acme/db/nosql/certificate.go b/acme/db/nosql/certificate.go index e69de29b..a008db07 100644 --- a/acme/db/nosql/certificate.go +++ b/acme/db/nosql/certificate.go @@ -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 + +} diff --git a/acme/order.go b/acme/order.go index 16a0ead2..8879fed0 100644 --- a/acme/order.go +++ b/acme/order.go @@ -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], diff --git a/acme/status.go b/acme/status.go index c98a506e..d9aae82d 100644 --- a/acme/status.go +++ b/acme/status.go @@ -1,4 +1,4 @@ -package types +package acme // Status represents an ACME status. type Status string