From 2ae43ef2dc386ec7dc6a75d5bb65e07c1d4d3799 Mon Sep 17 00:00:00 2001 From: max furman Date: Sun, 28 Feb 2021 22:49:20 -0800 Subject: [PATCH] [acme db interface] wip errors --- acme/account.go | 16 +- acme/api/account.go | 7 +- acme/authority.go | 67 ++-- acme/authorization.go | 19 +- acme/certificate.go | 5 +- acme/challenge.go | 38 +- acme/common.go | 33 -- acme/db.go | 2 +- acme/db/nosql/account.go | 52 +-- acme/db/nosql/authz.go | 67 ++-- acme/db/nosql/certificate.go | 68 ++-- acme/db/nosql/challenge.go | 92 +++-- acme/db/nosql/nonce.go | 14 +- acme/db/nosql/nosql.go | 51 ++- acme/db/nosql/order.go | 36 +- acme/errors.go | 632 ++++++++++++-------------------- acme/order.go | 73 ++-- authority/provisioner/method.go | 3 +- 18 files changed, 562 insertions(+), 713 deletions(-) diff --git a/acme/account.go b/acme/account.go index a0f88d49..80cc66ef 100644 --- a/acme/account.go +++ b/acme/account.go @@ -1,9 +1,10 @@ package acme import ( + "crypto" + "encoding/base64" "encoding/json" - "github.com/pkg/errors" "go.step.sm/crypto/jose" ) @@ -11,7 +12,7 @@ import ( // attributes required for responses in the ACME protocol. type Account struct { Contact []string `json:"contact,omitempty"` - Status string `json:"status"` + Status Status `json:"status"` Orders string `json:"orders"` ID string `json:"-"` Key *jose.JSONWebKey `json:"-"` @@ -21,7 +22,7 @@ type Account struct { 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 nil, ErrorWrap(ErrorServerInternalType, err, "error marshaling account for logging") } return string(b), nil } @@ -40,3 +41,12 @@ func (a *Account) GetKey() *jose.JSONWebKey { func (a *Account) IsValid() bool { return Status(a.Status) == StatusValid } + +// KeyToID converts a JWK to a thumbprint. +func KeyToID(jwk *jose.JSONWebKey) (string, error) { + kid, err := jwk.Thumbprint(crypto.SHA256) + if err != nil { + return "", ErrorWrap(ErrorServerInternalType, err, "error generating jwk thumbprint") + } + return base64.RawURLEncoding.EncodeToString(kid), nil +} diff --git a/acme/api/account.go b/acme/api/account.go index 93f46651..ec2854cc 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -44,7 +44,7 @@ type UpdateAccountRequest struct { // IsDeactivateRequest returns true if the update request is a deactivation // request, false otherwise. func (u *UpdateAccountRequest) IsDeactivateRequest() bool { - return u.Status == acme.StatusDeactivated + return u.Status == string(acme.StatusDeactivated) } // Validate validates a update-account request body. @@ -59,7 +59,7 @@ func (u *UpdateAccountRequest) Validate() error { } return nil case len(u.Status) > 0: - if u.Status != acme.StatusDeactivated { + if u.Status != string(acme.StatusDeactivated) { return acme.MalformedErr(errors.Errorf("cannot update account "+ "status to %s, only deactivated", u.Status)) } @@ -110,9 +110,10 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } - if acc, err = h.Auth.NewAccount(r.Context(), acme.AccountOptions{ + if acc, err = h.Auth.NewAccount(r.Context(), &acme.Account{ Key: jwk, Contact: nar.Contact, + Status: acme.StatusValid, }); err != nil { api.WriteError(w, err) return diff --git a/acme/authority.go b/acme/authority.go index d07f591a..77e031d0 100644 --- a/acme/authority.go +++ b/acme/authority.go @@ -2,10 +2,8 @@ package acme import ( "context" - "crypto" "crypto/tls" "crypto/x509" - "encoding/base64" "log" "net" "net/http" @@ -14,8 +12,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" - database "github.com/smallstep/certificates/db" - "github.com/smallstep/nosql" "go.step.sm/crypto/jose" ) @@ -49,7 +45,7 @@ type Interface interface { // Authority is the layer that handles all ACME interactions. type Authority struct { backdate provisioner.Duration - db nosql.DB + db DB dir *directory signAuth SignAuthority } @@ -57,8 +53,8 @@ type Authority struct { // AuthorityOptions required to create a new ACME Authority. type AuthorityOptions struct { Backdate provisioner.Duration - // DB is the database used by nosql. - DB nosql.DB + // DB storage backend that impements the acme.DB interface. + DB DB // DNS the host used to generate accurate ACME links. By default the authority // will use the Host from the request, so this value will only be used if // request.Host is empty. @@ -74,7 +70,7 @@ type AuthorityOptions struct { // // Deprecated: NewAuthority exists for hitorical compatibility and should not // be used. Use acme.New() instead. -func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority) (*Authority, error) { +func NewAuthority(db DB, dns, prefix string, signAuth SignAuthority) (*Authority, error) { return New(signAuth, AuthorityOptions{ DB: db, DNS: dns, @@ -84,19 +80,6 @@ func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority) (*Aut // New returns a new Authority that implements the ACME interface. func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) { - if _, ok := ops.DB.(*database.SimpleDB); !ok { - // If it's not a SimpleDB then go ahead and bootstrap the DB with the - // necessary ACME tables. SimpleDB should ONLY be used for testing. - tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, - challengeTable, nonceTable, orderTable, ordersByAccountIDTable, - certTable} - for _, b := range tables { - if err := ops.DB.CreateTable(b); err != nil { - return nil, errors.Wrapf(err, "error creating table %s", - string(b)) - } - } - } return &Authority{ backdate: ops.Backdate, db: ops.DB, dir: newDirectory(ops.DNS, ops.Prefix), signAuth: signAuth, }, nil @@ -130,21 +113,21 @@ func (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error } // NewNonce generates, stores, and returns a new ACME nonce. -func (a *Authority) NewNonce(ctx context.Context) (string, error) { +func (a *Authority) NewNonce(ctx context.Context) (Nonce, error) { return a.db.CreateNonce(ctx) } // UseNonce consumes the given nonce if it is valid, returns error otherwise. func (a *Authority) UseNonce(ctx context.Context, nonce string) error { - return a.db.DeleteNonce(ctx, nonce) + return a.db.DeleteNonce(ctx, Nonce(nonce)) } // NewAccount creates, stores, and returns a new ACME account. -func (a *Authority) NewAccount(ctx context.Context, acc *Account) (*Account, error) { +func (a *Authority) NewAccount(ctx context.Context, acc *Account) error { if err := a.db.CreateAccount(ctx, acc); err != nil { - return ServerInternalErr(err) + return ErrorWrap(ErrorServerInternalType, err, "newAccount: error creating account") } - return a, nil + return nil } // UpdateAccount updates an ACME account. @@ -153,8 +136,8 @@ func (a *Authority) UpdateAccount(ctx context.Context, acc *Account) (*Account, acc.Contact = auo.Contact acc.Status = auo.Status */ - if err = a.db.UpdateAccount(ctx, acc); err != nil { - return ServerInternalErr(err) + if err := a.db.UpdateAccount(ctx, acc); err != nil { + return nil, ErrorWrap(ErrorServerInternalType, err, "authority.UpdateAccount - database update failed" } return acc, nil } @@ -164,17 +147,9 @@ func (a *Authority) GetAccount(ctx context.Context, id string) (*Account, error) return a.db.GetAccount(ctx, id) } -func keyToID(jwk *jose.JSONWebKey) (string, error) { - kid, err := jwk.Thumbprint(crypto.SHA256) - if err != nil { - return "", ServerInternalErr(errors.Wrap(err, "error generating jwk thumbprint")) - } - return base64.RawURLEncoding.EncodeToString(kid), nil -} - // GetAccountByKey returns the ACME associated with the jwk id. func (a *Authority) GetAccountByKey(ctx context.Context, jwk *jose.JSONWebKey) (*Account, error) { - kid, err := keyToID(jwk) + kid, err := KeyToID(jwk) if err != nil { return nil, err } @@ -200,12 +175,13 @@ func (a *Authority) GetOrder(ctx context.Context, accID, orderID string) (*Order log.Printf("provisioner-id from request ('%s') does not match order provisioner-id ('%s')", prov.GetID(), o.ProvisionerID) return nil, UnauthorizedErr(errors.New("provisioner does not own order")) } - if err = a.updateOrderStatus(ctx, o); err != nil { + if err = o.UpdateStatus(ctx, a.db); err != nil { return nil, err } - return o.toACME(ctx, a.db, a.dir) + return o, nil } +/* // GetOrdersByAccount returns the list of order urls owned by the account. func (a *Authority) GetOrdersByAccount(ctx context.Context, id string) ([]string, error) { ordersByAccountMux.Lock() @@ -223,6 +199,7 @@ func (a *Authority) GetOrdersByAccount(ctx context.Context, id string) ([]string } return ret, nil } +*/ // NewOrder generates, stores, and returns a new ACME order. func (a *Authority) NewOrder(ctx context.Context, o *Order) (*Order, error) { @@ -234,7 +211,7 @@ func (a *Authority) NewOrder(ctx context.Context, o *Order) (*Order, error) { o.Backdate = a.backdate.Duration o.ProvisionerID = prov.GetID() - if err = db.CreateOrder(ctx, o); err != nil { + if err = a.db.CreateOrder(ctx, o); err != nil { return nil, ServerInternalErr(err) } return o, nil @@ -258,8 +235,7 @@ func (a *Authority) FinalizeOrder(ctx context.Context, accID, orderID string, cs log.Printf("provisioner-id from request ('%s') does not match order provisioner-id ('%s')", prov.GetID(), o.ProvisionerID) return nil, UnauthorizedErr(errors.New("provisioner does not own order")) } - o, err = o.Finalize(ctx, a.db, csr, a.signAuth, prov) - if err != nil { + if err = o.Finalize(ctx, a.db, csr, a.signAuth, prov); err != nil { return nil, Wrap(err, "error finalizing order") } return o, nil @@ -276,8 +252,7 @@ func (a *Authority) GetAuthz(ctx context.Context, accID, authzID string) (*Autho log.Printf("account-id from request ('%s') does not match authz account-id ('%s')", accID, az.AccountID) return nil, UnauthorizedErr(errors.New("account does not own authz")) } - az, err = az.UpdateStatus(ctx, a.db) - if err != nil { + if err = az.UpdateStatus(ctx, a.db); err != nil { return nil, Wrap(err, "error updating authz status") } return az, nil @@ -313,7 +288,7 @@ func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, j // GetCertificate retrieves the Certificate by ID. func (a *Authority) GetCertificate(ctx context.Context, accID, certID string) ([]byte, error) { - cert, err := a.db.GetCertificate(a.db, certID) + cert, err := a.db.GetCertificate(ctx, certID) if err != nil { return nil, err } @@ -321,5 +296,5 @@ func (a *Authority) GetCertificate(ctx context.Context, accID, certID string) ([ 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) + return cert.ToACME(ctx) } diff --git a/acme/authorization.go b/acme/authorization.go index f1ef0adc..43095fb3 100644 --- a/acme/authorization.go +++ b/acme/authorization.go @@ -11,7 +11,7 @@ import ( // Authorization representst an ACME Authorization. type Authorization struct { Identifier *Identifier `json:"identifier"` - Status string `json:"status"` + Status Status `json:"status"` Expires string `json:"expires"` Challenges []*Challenge `json:"challenges"` Wildcard bool `json:"wildcard"` @@ -34,7 +34,7 @@ func (az *Authorization) UpdateStatus(ctx context.Context, db DB) error { now := time.Now().UTC() expiry, err := time.Parse(time.RFC3339, az.Expires) if err != nil { - return ServerInternalErr(errors.Wrap("error converting expiry string to time")) + return ServerInternalErr(errors.Wrap(err, "error converting expiry string to time")) } switch az.Status { @@ -46,16 +46,11 @@ func (az *Authorization) UpdateStatus(ctx context.Context, db DB) error { // check expiry if now.After(expiry) { az.Status = StatusInvalid - az.Error = MalformedErr(errors.New("authz has expired")) break } var isValid = false - for _, chID := range ba.Challenges { - ch, err := db.GetChallenge(ctx, chID, az.ID) - if err != nil { - return ServerInternalErr(err) - } + for _, ch := range az.Challenges { if ch.Status == StatusValid { isValid = true break @@ -66,10 +61,12 @@ func (az *Authorization) UpdateStatus(ctx context.Context, db DB) error { return nil } az.Status = StatusValid - az.Error = nil default: - return nil, ServerInternalErr(errors.Errorf("unrecognized authz status: %s", ba.Status)) + return ServerInternalErr(errors.Errorf("unrecognized authorization status: %s", az.Status)) } - return ServerInternalErr(db.UpdateAuthorization(ctx, az)) + if err = db.UpdateAuthorization(ctx, az); err != nil { + return ServerInternalErr(err) + } + return nil } diff --git a/acme/certificate.go b/acme/certificate.go index f088d93c..356c0121 100644 --- a/acme/certificate.go +++ b/acme/certificate.go @@ -1,10 +1,9 @@ package acme import ( + "context" "crypto/x509" "encoding/pem" - - "github.com/smallstep/nosql" ) // Certificate options with which to create and store a cert object. @@ -17,7 +16,7 @@ type Certificate struct { } // ToACME encodes the entire X509 chain into a PEM list. -func (cert *Certificate) ToACME(db nosql.DB, dir *directory) ([]byte, error) { +func (cert *Certificate) ToACME(ctx context.Context) ([]byte, error) { var ret []byte for _, c := range append([]*x509.Certificate{cert.Leaf}, cert.Intermediates...) { ret = append(ret, pem.EncodeToMemory(&pem.Block{ diff --git a/acme/challenge.go b/acme/challenge.go index e7abaf64..59ca454a 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -18,14 +18,13 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/nosql" "go.step.sm/crypto/jose" ) // Challenge represents an ACME response Challenge type. type Challenge struct { Type string `json:"type"` - Status string `json:"status"` + Status Status `json:"status"` Token string `json:"token"` Validated string `json:"validated,omitempty"` URL string `json:"url"` @@ -99,7 +98,7 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb // Update and store the challenge. ch.Status = StatusValid ch.Error = nil - ch.Validated = clock.Now() + ch.Validated = clock.Now().Format(time.RFC3339) return ServerInternalErr(db.UpdateChallenge(ctx, ch)) } @@ -107,11 +106,11 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo validateOptions) error { config := &tls.Config{ NextProtos: []string{"acme-tls/1"}, - ServerName: tc.Value, + ServerName: ch.Value, InsecureSkipVerify: true, // we expect a self-signed challenge certificate } - hostPort := net.JoinHostPort(tc.Value, "443") + hostPort := net.JoinHostPort(ch.Value, "443") conn, err := vo.tlsDial("tcp", hostPort, config) if err != nil { @@ -125,7 +124,7 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON if len(certs) == 0 { return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("%s "+ - "challenge for %s resulted in no certificates", tc.Type, tc.Value))) + "challenge for %s resulted in no certificates", ch.Type, ch.Value))) } if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != "acme-tls/1" { @@ -135,18 +134,18 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON leafCert := certs[0] - if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], tc.Value) { + if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], ch.Value) { return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ - "leaf certificate must contain a single DNS name, %v", tc.Value))) + "leaf certificate must contain a single DNS name, %v", ch.Value))) } idPeAcmeIdentifier := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} idPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} foundIDPeAcmeIdentifierV1Obsolete := false - keyAuth, err := KeyAuthorization(tc.Token, jwk) + keyAuth, err := KeyAuthorization(ch.Token, jwk) if err != nil { - return nil, err + return err } hashedKeyAuth := sha256.Sum256([]byte(keyAuth)) @@ -173,9 +172,12 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON ch.Status = StatusValid ch.Error = nil - ch.Validated = clock.Now() + ch.Validated = clock.Now().Format(time.RFC3339) - return ServerInternalErr(db.UpdateChallenge(ctx, ch)) + if err = db.UpdateChallenge(ctx, ch); err != nil { + return ServerInternalErr(errors.Wrap(err, "tlsalpn01ValidateChallenge - error updating challenge")) + } + return nil } if idPeAcmeIdentifierV1Obsolete.Equal(ext.Id) { @@ -192,12 +194,12 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON "certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension"))) } -func dns01Validate(ctx context.Context, ch *Challenge, db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) error { +func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo validateOptions) error { // Normalize domain for wildcard DNS names // This is done to avoid making TXT lookups for domains like // _acme-challenge.*.example.com // Instead perform txt lookup for _acme-challenge.example.com - domain := strings.TrimPrefix(dc.Value, "*.") + domain := strings.TrimPrefix(ch.Value, "*.") txtRecords, err := vo.lookupTxt("_acme-challenge." + domain) if err != nil { @@ -205,9 +207,9 @@ func dns01Validate(ctx context.Context, ch *Challenge, db nosql.DB, jwk *jose.JS "records for domain %s", domain))) } - expectedKeyAuth, err := KeyAuthorization(dc.Token, jwk) + expectedKeyAuth, err := KeyAuthorization(ch.Token, jwk) if err != nil { - return nil, err + return err } h := sha256.Sum256([]byte(expectedKeyAuth)) expected := base64.RawURLEncoding.EncodeToString(h[:]) @@ -226,7 +228,7 @@ func dns01Validate(ctx context.Context, ch *Challenge, db nosql.DB, jwk *jose.JS // Update and store the challenge. ch.Status = StatusValid ch.Error = nil - ch.Validated = time.Now().UTC() + ch.Validated = clock.Now().UTC().Format(time.RFC3339) return ServerInternalErr(db.UpdateChallenge(ctx, ch)) } @@ -243,7 +245,7 @@ func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) { } // storeError the given error to an ACME error and saves using the DB interface. -func (bc *baseChallenge) storeError(ctx context.Context, ch Challenge, db nosql.DB, err *Error) error { +func storeError(ctx context.Context, ch *Challenge, db DB, err *Error) error { ch.Error = err.ToACME() if err := db.UpdateChallenge(ctx, ch); err != nil { return ServerInternalErr(errors.Wrap(err, "failure saving error to acme challenge")) diff --git a/acme/common.go b/acme/common.go index a5a1fe09..1b268327 100644 --- a/acme/common.go +++ b/acme/common.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" - "go.step.sm/crypto/randutil" ) // Provisioner is an interface that implements a subset of the provisioner.Interface -- @@ -149,38 +148,6 @@ type SignAuthority interface { LoadProvisionerByID(string) (provisioner.Interface, error) } -// Identifier encodes the type that an order pertains to. -type Identifier struct { - Type string `json:"type"` - Value string `json:"value"` -} - -var ( - // StatusValid -- valid - StatusValid = "valid" - // StatusInvalid -- invalid - StatusInvalid = "invalid" - // StatusPending -- pending; e.g. an Order that is not ready to be finalized. - StatusPending = "pending" - // StatusDeactivated -- deactivated; e.g. for an Account that is not longer valid. - StatusDeactivated = "deactivated" - // StatusReady -- ready; e.g. for an Order that is ready to be finalized. - StatusReady = "ready" - //statusExpired = "expired" - //statusActive = "active" - //statusProcessing = "processing" -) - -var idLen = 32 - -func randID() (val string, err error) { - val, err = randutil.Alphanumeric(idLen) - if err != nil { - return "", ServerInternalErr(errors.Wrap(err, "error generating random alphanumeric ID")) - } - return val, nil -} - // Clock that returns time in UTC rounded to seconds. type Clock int diff --git a/acme/db.go b/acme/db.go index 846eed04..dfbd30ce 100644 --- a/acme/db.go +++ b/acme/db.go @@ -4,7 +4,7 @@ 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) + CreateAccount(ctx context.Context, acc *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 diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index e863c371..40961ce3 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -6,6 +6,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" nosqlDB "github.com/smallstep/nosql" "go.step.sm/crypto/jose" ) @@ -17,7 +18,7 @@ type dbAccount struct { Deactivated time.Time `json:"deactivated"` Key *jose.JSONWebKey `json:"key"` Contact []string `json:"contact,omitempty"` - Status string `json:"status"` + Status acme.Status `json:"status"` } func (dba *dbAccount) clone() *dbAccount { @@ -26,33 +27,34 @@ func (dba *dbAccount) clone() *dbAccount { } // CreateAccount imlements the AcmeDB.CreateAccount interface. -func (db *DB) CreateAccount(ctx context.Context, acc *Account) error { +func (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error { + var err error acc.ID, err = randID() if err != nil { - return nil, err + return err } dba := &dbAccount{ ID: acc.ID, Key: acc.Key, Contact: acc.Contact, - Status: acc.Valid, + Status: acc.Status, Created: clock.Now(), } - kid, err := keyToID(dba.Key) + kid, err := acme.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)) + _, swapped, err := db.db.CmpAndSwap(accountByKeyIDTable, kidB, nil, []byte(acc.ID)) switch { case err != nil: - return ServerInternalErr(errors.Wrap(err, "error setting key-id to account-id index")) + return errors.Wrap(err, "error storing keyID to accountID index") case !swapped: - return ServerInternalErr(errors.Errorf("key-id to account-id index already exists")) + return errors.Errorf("key-id to account-id index already exists") default: if err = db.save(ctx, acc.ID, dba, nil, "account", accountTable); err != nil { db.db.Del(accountByKeyIDTable, kidB) @@ -63,24 +65,24 @@ func (db *DB) CreateAccount(ctx context.Context, acc *Account) error { } // GetAccount retrieves an ACME account by ID. -func (db *DB) GetAccount(ctx context.Context, id string) (*Account, error) { - acc, err := db.getDBAccount(ctx, id) +func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) { + dbacc, err := db.getDBAccount(ctx, id) if err != nil { return nil, err } - return &Account{ + return &acme.Account{ Status: dbacc.Status, Contact: dbacc.Contact, - Orders: dir.getLink(ctx, OrdersByAccountLink, true, a.ID), + Orders: dir.getLink(ctx, OrdersByAccountLink, true, dbacc.ID), Key: dbacc.Key, ID: dbacc.ID, }, nil } // GetAccountByKeyID retrieves an ACME account by KeyID (thumbprint of the Account Key -- JWK). -func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) { - id, err := db.getAccountIDByKeyID(kid) +func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*acme.Account, error) { + id, err := db.getAccountIDByKeyID(ctx, kid) if err != nil { return nil, err } @@ -88,9 +90,9 @@ func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*Account, erro } // UpdateAccount imlements the AcmeDB.UpdateAccount interface. -func (db *DB) UpdateAccount(ctx context.Context, acc *Account) error { +func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error { if len(acc.ID) == 0 { - return ServerInternalErr(errors.New("id cannot be empty")) + return errors.New("id cannot be empty") } old, err := db.getDBAccount(ctx, acc.ID) @@ -99,24 +101,24 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *Account) error { } nu := old.clone() - nu.Contact = acc.contact + nu.Contact = acc.Contact nu.Status = acc.Status // If the status has changed to 'deactivated', then set deactivatedAt timestamp. - if acc.Status == StatusDeactivated && old.Status != Status.Deactivated { + if acc.Status == acme.StatusDeactivated && old.Status != acme.StatusDeactivated { nu.Deactivated = clock.Now() } - return db.save(ctx, old.ID, newdba, dba, "account", accountTable) + return db.save(ctx, old.ID, nu, old, "account", accountTable) } func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, 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 "", errors.Wrapf(err, "account with key id %s not found", kid) } - return nil, ServerInternalErr(errors.Wrapf(err, "error loading key-account index")) + return "", errors.Wrapf(err, "error loading key-account index") } return string(id), nil } @@ -126,14 +128,14 @@ func (db *DB) getDBAccount(ctx context.Context, id string) (*dbAccount, error) { data, 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, errors.Wrapf(err, "account %s not found", id) } - return nil, ServerInternalErr(errors.Wrapf(err, "error loading account %s", id)) + return nil, errors.Wrapf(err, "error loading account %s", id) } - dbacc := new(account) + dbacc := new(dbAccount) if err = json.Unmarshal(data, dbacc); err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling account")) + return nil, errors.Wrap(err, "error unmarshaling account") } return dbacc, nil } diff --git a/acme/db/nosql/authz.go b/acme/db/nosql/authz.go index a50d46f1..bc9f75bc 100644 --- a/acme/db/nosql/authz.go +++ b/acme/db/nosql/authz.go @@ -7,6 +7,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" "github.com/smallstep/nosql" ) @@ -14,15 +15,15 @@ var defaultExpiryDuration = time.Hour * 24 // dbAuthz is the base authz type that others build from. type dbAuthz struct { - ID string `json:"id"` - AccountID string `json:"accountID"` - Identifier *Identifier `json:"identifier"` - Status string `json:"status"` - Expires time.Time `json:"expires"` - Challenges []string `json:"challenges"` - Wildcard bool `json:"wildcard"` - Created time.Time `json:"created"` - Error *Error `json:"error"` + ID string `json:"id"` + AccountID string `json:"accountID"` + Identifier *acme.Identifier `json:"identifier"` + Status acme.Status `json:"status"` + Expires time.Time `json:"expires"` + Challenges []string `json:"challenges"` + Wildcard bool `json:"wildcard"` + Created time.Time `json:"created"` + Error *acme.Error `json:"error"` } func (ba *dbAuthz) clone() *dbAuthz { @@ -35,33 +36,33 @@ func (ba *dbAuthz) clone() *dbAuthz { func (db *DB) getDBAuthz(ctx context.Context, id string) (*dbAuthz, error) { data, err := db.db.Get(authzTable, []byte(id)) if nosql.IsErrNotFound(err) { - return nil, MalformedErr(errors.Wrapf(err, "authz %s not found", id)) + return nil, errors.Wrapf(err, "authz %s not found", id) } else if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error loading authz %s", id)) + return nil, errors.Wrapf(err, "error loading authz %s", id) } var dbaz dbAuthz if err = json.Unmarshal(data, &dbaz); err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling authz type into dbAuthz")) + return nil, errors.Wrap(err, "error unmarshaling authz type into dbAuthz") } - return &dbaz + return &dbaz, nil } // GetAuthorization retrieves and unmarshals an ACME authz type from the database. // Implements acme.DB GetAuthorization interface. -func (db *DB) GetAuthorization(ctx context.Context, id string) (*types.Authorization, error) { - dbaz, err := getDBAuthz(id) +func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorization, error) { + dbaz, err := db.getDBAuthz(ctx, id) if err != nil { return nil, err } - var chs = make([]*Challenge, len(ba.Challenges)) + var chs = make([]*acme.Challenge, len(dbaz.Challenges)) for i, chID := range dbaz.Challenges { - chs[i], err = db.GetChallenge(ctx, chID) + chs[i], err = db.GetChallenge(ctx, chID, id) if err != nil { return nil, err } } - return &types.Authorization{ + return &acme.Authorization{ Identifier: dbaz.Identifier, Status: dbaz.Status, Challenges: chs, @@ -73,23 +74,24 @@ func (db *DB) GetAuthorization(ctx context.Context, id string) (*types.Authoriza // CreateAuthorization creates an entry in the database for the Authorization. // Implements the acme.DB.CreateAuthorization interface. -func (db *DB) CreateAuthorization(ctx context.Context, az *types.Authorization) error { +func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) error { if len(az.AccountID) == 0 { - return ServerInternalErr(errors.New("account-id cannot be empty")) + return errors.New("account-id cannot be empty") } if az.Identifier == nil { - return ServerInternalErr(errors.New("identifier cannot be nil")) + return errors.New("identifier cannot be nil") } + var err error az.ID, err = randID() if err != nil { - return nil, err + return err } now := clock.Now() dbaz := &dbAuthz{ ID: az.ID, AccountID: az.AccountID, - Status: types.StatusPending, + Status: acme.StatusPending, Created: now, Expires: now.Add(defaultExpiryDuration), Identifier: az.Identifier, @@ -97,9 +99,9 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *types.Authorization) if strings.HasPrefix(az.Identifier.Value, "*.") { dbaz.Wildcard = true - dbaz.Identifier = Identifier{ - Value: strings.TrimPrefix(identifier.Value, "*."), - Type: identifier.Type, + dbaz.Identifier = &acme.Identifier{ + Value: strings.TrimPrefix(az.Identifier.Value, "*."), + Type: az.Identifier.Type, } } @@ -111,14 +113,14 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *types.Authorization) } for _, typ := range chTypes { - ch, err := db.CreateChallenge(ctx, &types.Challenge{ + ch := &acme.Challenge{ AccountID: az.AccountID, AuthzID: az.ID, Value: az.Identifier.Value, Type: typ, - }) - if err != nil { - return nil, Wrapf(err, "error creating '%s' challenge", typ) + } + if err = db.CreateChallenge(ctx, ch); err != nil { + return errors.Wrapf(err, "error creating challenge") } chIDs = append(chIDs, ch.ID) @@ -129,9 +131,9 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *types.Authorization) } // UpdateAuthorization saves an updated ACME Authorization to the database. -func (db *DB) UpdateAuthorization(ctx context.Context, az *types.Authorization) error { +func (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) error { if len(az.ID) == 0 { - return ServerInternalErr(errors.New("id cannot be empty")) + return errors.New("id cannot be empty") } old, err := db.getDBAuthz(ctx, az.ID) if err != nil { @@ -141,6 +143,5 @@ func (db *DB) UpdateAuthorization(ctx context.Context, az *types.Authorization) nu := old.clone() nu.Status = az.Status - nu.Error = az.Error return db.save(ctx, old.ID, nu, old, "authz", authzTable) } diff --git a/acme/db/nosql/certificate.go b/acme/db/nosql/certificate.go index a008db07..ef766843 100644 --- a/acme/db/nosql/certificate.go +++ b/acme/db/nosql/certificate.go @@ -8,6 +8,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" "github.com/smallstep/nosql" ) @@ -21,25 +22,26 @@ type dbCert struct { } // CreateCertificate creates and stores an ACME certificate type. -func (db *DB) CreateCertificate(ctx context.Context, cert *Certificate) error { - cert.id, err = randID() +func (db *DB) CreateCertificate(ctx context.Context, cert *acme.Certificate) error { + var err error + cert.ID, err = randID() if err != nil { return err } leaf := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", - Bytes: ops.Leaf.Raw, + Bytes: cert.Leaf.Raw, }) var intermediates []byte - for _, cert := range ops.Intermediates { + for _, cert := range cert.Intermediates { intermediates = append(intermediates, pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, })...) } - cert := &dbCert{ + dbch := &dbCert{ ID: cert.ID, AccountID: cert.AccountID, OrderID: cert.OrderID, @@ -47,74 +49,80 @@ func (db *DB) CreateCertificate(ctx context.Context, cert *Certificate) error { Intermediates: intermediates, Created: time.Now().UTC(), } - return db.save(ctx, cert.ID, cert, nil, "certificate", certTable) + return db.save(ctx, cert.ID, dbch, 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) { +func (db *DB) GetCertificate(ctx context.Context, id string) (*acme.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)) + return nil, errors.Wrapf(err, "certificate %s not found", id) } else if err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error loading certificate")) + return nil, 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")) + dbC := new(dbCert) + if err := json.Unmarshal(b, dbC); err != nil { + return nil, errors.Wrap(err, "error unmarshaling certificate") } - leaf, err := parseCert(dbCert.Leaf) + leaf, err := parseCert(dbC.Leaf) if err != nil { - return nil, ServerInternalErr(errors.Wrapf("error parsing leaf of ACME Certificate with ID '%s'", id)) + return nil, errors.Wrapf(err, "error parsing leaf of ACME Certificate with ID '%s'", id) } - intermediates, err := parseBundle(dbCert.Intermediates) + intermediates, err := parseBundle(dbC.Intermediates) if err != nil { - return nil, ServerInternalErr(errors.Wrapf("error parsing intermediate bundle of ACME Certificate with ID '%s'", id)) + return nil, errors.Wrapf(err, "error parsing intermediate bundle of ACME Certificate with ID '%s'", id) } - return &Certificate{ - ID: dbCert.ID, - AccountID: dbCert.AccountID, - OrderID: dbCert.OrderID, + return &acme.Certificate{ + ID: dbC.ID, + AccountID: dbC.AccountID, + OrderID: dbC.OrderID, Leaf: leaf, - Intermediates: intermediate, - } + Intermediates: intermediates, + }, nil } func parseCert(b []byte) (*x509.Certificate, error) { - block, rest := pem.Decode(dbCert.Leaf) + block, rest := pem.Decode(b) 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) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing x509 certificate") + } + return cert, nil } func parseBundle(b []byte) ([]*x509.Certificate, error) { - var block *pem.Block - var bundle []*x509.Certificate + var ( + err error + block *pem.Block + 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) + return nil, errors.New("error decoding PEM: data contains block that is not a certificate") } var crt *x509.Certificate crt, err = x509.ParseCertificate(block.Bytes) if err != nil { - return nil, errors.Wrapf(err, "error parsing %s", filename) + return nil, errors.Wrapf(err, "error parsing x509 certificate") } bundle = append(bundle, crt) } if len(b) > 0 { - return nil, errors.Errorf("error decoding PEM: file '%s' contains unexpected data", filename) + return nil, errors.New("error decoding PEM: unexpected data") } return bundle, nil diff --git a/acme/db/nosql/challenge.go b/acme/db/nosql/challenge.go index bd3be0d0..62513778 100644 --- a/acme/db/nosql/challenge.go +++ b/acme/db/nosql/challenge.go @@ -6,75 +6,69 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" "github.com/smallstep/nosql" ) -// ChallengeOptions is the type used to created a new Challenge. -type ChallengeOptions struct { - AccountID string - AuthzID string - Identifier Identifier -} - // dbChallenge is the base Challenge type that others build from. type dbChallenge struct { - ID string `json:"id"` - AccountID string `json:"accountID"` - AuthzID string `json:"authzID"` - Type string `json:"type"` - Status string `json:"status"` - Token string `json:"token"` - Value string `json:"value"` - Validated time.Time `json:"validated"` - Created time.Time `json:"created"` - Error *AError `json:"error"` + ID string `json:"id"` + AccountID string `json:"accountID"` + AuthzID string `json:"authzID"` + Type string `json:"type"` + Status acme.Status `json:"status"` + Token string `json:"token"` + Value string `json:"value"` + Validated string `json:"validated"` + Created time.Time `json:"created"` + Error *AError `json:"error"` } func (dbc *dbChallenge) clone() *dbChallenge { - u := *bc + u := *dbc return &u } func (db *DB) getDBChallenge(ctx context.Context, id string) (*dbChallenge, error) { data, err := db.db.Get(challengeTable, []byte(id)) if nosql.IsErrNotFound(err) { - return nil, MalformedErr(errors.Wrapf(err, "challenge %s not found", id)) + return nil, errors.Wrapf(err, "challenge %s not found", id) } else if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error loading challenge %s", id)) + return nil, errors.Wrapf(err, "error loading challenge %s", id) } - dbch := new(baseChallenge) + dbch := new(dbChallenge) if err := json.Unmarshal(data, dbch); err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling "+ - "challenge type into dbChallenge")) + return nil, errors.Wrap(err, "error unmarshaling dbChallenge") } - return dbch + return dbch, nil } // CreateChallenge creates a new ACME challenge data structure in the database. // Implements acme.DB.CreateChallenge interface. -func (db *DB) CreateChallenge(ctx context.context, ch *types.Challenge) error { +func (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error { if len(ch.AuthzID) == 0 { - return ServerInternalError(errors.New("AuthzID cannot be empty")) + return errors.New("AuthzID cannot be empty") } if len(ch.AccountID) == 0 { - return ServerInternalError(errors.New("AccountID cannot be empty")) + return errors.New("AccountID cannot be empty") } if len(ch.Value) == 0 { - return ServerInternalError(errors.New("AccountID cannot be empty")) + return errors.New("AccountID cannot be empty") } // TODO: verify that challenge type is set and is one of expected types. if len(ch.Type) == 0 { - return ServerInternalError(errors.New("Type cannot be empty")) + return errors.New("Type cannot be empty") } + var err error ch.ID, err = randID() if err != nil { - return nil, Wrap(err, "error generating random id for ACME challenge") + return errors.Wrap(err, "error generating random id for ACME challenge") } ch.Token, err = randID() if err != nil { - return nil, Wrap(err, "error generating token for ACME challenge") + return errors.Wrap(err, "error generating token for ACME challenge") } dbch := &dbChallenge{ @@ -82,42 +76,40 @@ func (db *DB) CreateChallenge(ctx context.context, ch *types.Challenge) error { AuthzID: ch.AuthzID, AccountID: ch.AccountID, Value: ch.Value, - Status: types.StatusPending, + Status: acme.StatusPending, Token: ch.Token, Created: clock.Now(), Type: ch.Type, } - return dbch.save(ctx, ch.ID, dbch, nil, "challenge", challengeTable) + return db.save(ctx, ch.ID, dbch, nil, "challenge", challengeTable) } // GetChallenge retrieves and unmarshals an ACME challenge type from the database. // Implements the acme.DB GetChallenge interface. -func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*types.Challenge, error) { +func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*acme.Challenge, error) { dbch, err := db.getDBChallenge(ctx, id) if err != nil { - return err + return nil, err } - ch := &Challenge{ - Type: dbch.Type, - Status: dbch.Status, - Token: dbch.Token, - URL: dir.getLink(ctx, ChallengeLink, true, dbch.getID()), - ID: dbch.ID, - AuthzID: dbch.AuthzID(), - Error: dbch.Error, - } - if !dbch.Validated.IsZero() { - ac.Validated = dbch.Validated.Format(time.RFC3339) + ch := &acme.Challenge{ + Type: dbch.Type, + Status: dbch.Status, + Token: dbch.Token, + URL: dir.getLink(ctx, ChallengeLink, true, dbch.ID), + ID: dbch.ID, + AuthzID: dbch.AuthzID, + Error: dbch.Error, + Validated: dbch.Validated, } return ch, nil } // UpdateChallenge updates an ACME challenge type in the database. -func (db *DB) UpdateChallenge(ctx context.Context, ch *types.Challenge) error { +func (db *DB) UpdateChallenge(ctx context.Context, ch *acme.Challenge) error { if len(ch.ID) == 0 { - return ServerInternalErr(errors.New("id cannot be empty")) + return errors.New("id cannot be empty") } old, err := db.getDBChallenge(ctx, ch.ID) if err != nil { @@ -129,9 +121,7 @@ func (db *DB) UpdateChallenge(ctx context.Context, ch *types.Challenge) error { // These should be the only values chaning in an Update request. nu.Status = ch.Status nu.Error = ch.Error - if nu.Status == types.StatusValid { - nu.Validated = clock.Now() - } + nu.Validated = ch.Validated return db.save(ctx, old.ID, nu, old, "challenge", challengeTable) } diff --git a/acme/db/nosql/nonce.go b/acme/db/nosql/nonce.go index f8f57f89..02dcda6c 100644 --- a/acme/db/nosql/nonce.go +++ b/acme/db/nosql/nonce.go @@ -1,11 +1,13 @@ package nosql import ( + "context" "encoding/base64" "encoding/json" "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" nosqlDB "github.com/smallstep/nosql" "github.com/smallstep/nosql/database" ) @@ -18,10 +20,10 @@ type dbNonce struct { // CreateNonce creates, stores, and returns an ACME replay-nonce. // Implements the acme.DB interface. -func (db *DB) CreateNonce() (Nonce, error) { +func (db *DB) CreateNonce(ctx context.Context) (acme.Nonce, error) { _id, err := randID() if err != nil { - return nil, err + return "", err } id := base64.RawURLEncoding.EncodeToString([]byte(_id)) @@ -31,12 +33,12 @@ func (db *DB) CreateNonce() (Nonce, error) { } b, err := json.Marshal(n) if err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error marshaling nonce")) + return "", errors.Wrap(err, "error marshaling nonce") } if err = db.save(ctx, id, b, nil, "nonce", nonceTable); err != nil { return "", err } - return Nonce(id), nil + return acme.Nonce(id), nil } // DeleteNonce verifies that the nonce is valid (by checking if it exists), @@ -59,9 +61,9 @@ func (db *DB) DeleteNonce(nonce string) error { switch { case nosqlDB.IsErrNotFound(err): - return BadNonceErr(nil) + return errors.New("not found") case err != nil: - return ServerInternalErr(errors.Wrapf(err, "error deleting nonce %s", nonce)) + return 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 index e11b92b2..0c040a89 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -3,9 +3,11 @@ package nosql import ( "context" "encoding/json" + "time" "github.com/pkg/errors" nosqlDB "github.com/smallstep/nosql" + "go.step.sm/crypto/randutil" ) var ( @@ -24,13 +26,26 @@ type DB struct { db nosqlDB.DB } +// New configures and returns a new ACME DB backend implemented using a nosql DB. +func New(db nosqlDB.DB) (*DB, error) { + tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, + challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable} + for _, b := range tables { + if err := db.CreateTable(b); err != nil { + return nil, errors.Wrapf(err, "error creating table %s", + string(b)) + } + } + return &DB{db}, nil +} + // save writes the new data to the database, overwriting the old data if it // existed. func (db *DB) save(ctx context.Context, id string, nu interface{}, old interface{}, typ string, table []byte) error { newB, err := json.Marshal(nu) if err != nil { - return ServerInternalErr(errors.Wrapf(err, - "error marshaling new acme %s", typ)) + return errors.Wrapf(err, + "error marshaling new acme %s", typ) } var oldB []byte if old == nil { @@ -38,19 +53,39 @@ func (db *DB) save(ctx context.Context, id string, nu interface{}, old interface } else { oldB, err = json.Marshal(old) if err != nil { - return ServerInternalErr(errors.Wrapf(err, - "error marshaling old acme %s", typ)) + return errors.Wrapf(err, + "error marshaling old acme %s", typ) } } - _, swapped, err := db.CmpAndSwap(table, []byte(id), oldB, newB) + _, swapped, err := db.db.CmpAndSwap(table, []byte(id), oldB, newB) switch { case err != nil: - return ServerInternalErr(errors.Wrapf(err, "error saving acme %s", typ)) + return errors.Wrapf(err, "error saving acme %s", typ) case !swapped: - return ServerInternalErr(errors.Errorf("error saving acme %s; "+ - "changed since last read", typ)) + return errors.Errorf("error saving acme %s; "+ + "changed since last read", typ) default: return nil } } + +var idLen = 32 + +func randID() (val string, err error) { + val, err = randutil.Alphanumeric(idLen) + if err != nil { + return "", errors.Wrap(err, "error generating random alphanumeric ID") + } + return val, nil +} + +// Clock that returns time in UTC rounded to seconds. +type Clock int + +// Now returns the UTC time rounded to seconds. +func (c *Clock) Now() time.Time { + return time.Now().UTC().Round(time.Second) +} + +var clock = new(Clock) diff --git a/acme/db/nosql/order.go b/acme/db/nosql/order.go index a0ab60da..528619d4 100644 --- a/acme/db/nosql/order.go +++ b/acme/db/nosql/order.go @@ -17,51 +17,51 @@ var defaultOrderExpiry = time.Hour * 24 var ordersByAccountMux sync.Mutex type dbOrder struct { - ID string `json:"id"` - AccountID string `json:"accountID"` - ProvisionerID string `json:"provisionerID"` - Created time.Time `json:"created"` - Expires time.Time `json:"expires,omitempty"` - Status string `json:"status"` - Identifiers []Identifier `json:"identifiers"` - NotBefore time.Time `json:"notBefore,omitempty"` - NotAfter time.Time `json:"notAfter,omitempty"` - Error *Error `json:"error,omitempty"` - Authorizations []string `json:"authorizations"` - Certificate string `json:"certificate,omitempty"` + ID string `json:"id"` + AccountID string `json:"accountID"` + ProvisionerID string `json:"provisionerID"` + Created time.Time `json:"created"` + Expires time.Time `json:"expires,omitempty"` + Status acme.Status `json:"status"` + Identifiers []acme.Identifier `json:"identifiers"` + NotBefore time.Time `json:"notBefore,omitempty"` + NotAfter time.Time `json:"notAfter,omitempty"` + Error *Error `json:"error,omitempty"` + Authorizations []string `json:"authorizations"` + Certificate string `json:"certificate,omitempty"` } // getDBOrder retrieves and unmarshals an ACME Order type from the database. func (db *DB) getDBOrder(id string) (*dbOrder, error) { b, err := db.db.Get(orderTable, []byte(id)) if nosql.IsErrNotFound(err) { - return nil, MalformedErr(errors.Wrapf(err, "order %s not found", id)) + return nil, errors.Wrapf(err, "order %s not found", id) } else if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error loading order %s", id)) + return nil, errors.Wrapf(err, "error loading order %s", id) } o := new(dbOrder) if err := json.Unmarshal(b, &o); err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling order")) + return nil, errors.Wrap(err, "error unmarshaling order") } return o, nil } // GetOrder retrieves an ACME Order from the database. -func (db *DB) GetOrder(id string) (*acme.Order, error) { +func (db *DB) GetOrder(ctx context.Context, id string) (*acme.Order, error) { dbo, err := db.getDBOrder(id) azs := make([]string, len(dbo.Authorizations)) for i, aid := range dbo.Authorizations { azs[i] = dir.getLink(ctx, AuthzLink, true, aid) } - o := &Order{ + o := &acme.Order{ Status: dbo.Status, Expires: dbo.Expires.Format(time.RFC3339), Identifiers: dbo.Identifiers, NotBefore: dbo.NotBefore.Format(time.RFC3339), NotAfter: dbo.NotAfter.Format(time.RFC3339), Authorizations: azs, - Finalize: dir.getLink(ctx, FinalizeLink, true, o.ID), + FinalizeURL: dir.getLink(ctx, FinalizeLink, true, o.ID), ID: dbo.ID, ProvisionerID: dbo.ProvisionerID, } diff --git a/acme/errors.go b/acme/errors.go index 9bd9c400..dc5b5568 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -1,410 +1,324 @@ +// Error represents an ACME package acme import ( + "fmt" + "github.com/pkg/errors" ) -// AccountDoesNotExistErr returns a new acme error. -func AccountDoesNotExistErr(err error) *Error { - return &Error{ - Type: accountDoesNotExistErr, - Detail: "Account does not exist", - Status: 400, - Err: err, - } -} - -// AlreadyRevokedErr returns a new acme error. -func AlreadyRevokedErr(err error) *Error { - return &Error{ - Type: alreadyRevokedErr, - Detail: "Certificate already revoked", - Status: 400, - Err: err, - } -} - -// BadCSRErr returns a new acme error. -func BadCSRErr(err error) *Error { - return &Error{ - Type: badCSRErr, - Detail: "The CSR is unacceptable", - Status: 400, - Err: err, - } -} - -// BadNonceErr returns a new acme error. -func BadNonceErr(err error) *Error { - return &Error{ - Type: badNonceErr, - Detail: "Unacceptable anti-replay nonce", - Status: 400, - Err: err, - } -} - -// BadPublicKeyErr returns a new acme error. -func BadPublicKeyErr(err error) *Error { - return &Error{ - Type: badPublicKeyErr, - Detail: "The jws was signed by a public key the server does not support", - Status: 400, - Err: err, - } -} - -// BadRevocationReasonErr returns a new acme error. -func BadRevocationReasonErr(err error) *Error { - return &Error{ - Type: badRevocationReasonErr, - Detail: "The revocation reason provided is not allowed by the server", - Status: 400, - Err: err, - } -} - -// BadSignatureAlgorithmErr returns a new acme error. -func BadSignatureAlgorithmErr(err error) *Error { - return &Error{ - Type: badSignatureAlgorithmErr, - Detail: "The JWS was signed with an algorithm the server does not support", - Status: 400, - Err: err, - } -} - -// CaaErr returns a new acme error. -func CaaErr(err error) *Error { - return &Error{ - Type: caaErr, - Detail: "Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate", - Status: 400, - Err: err, - } -} - -// CompoundErr returns a new acme error. -func CompoundErr(err error) *Error { - return &Error{ - Type: compoundErr, - Detail: "Specific error conditions are indicated in the “subproblems” array", - Status: 400, - Err: err, - } -} - -// ConnectionErr returns a new acme error. -func ConnectionErr(err error) *Error { - return &Error{ - Type: connectionErr, - Detail: "The server could not connect to validation target", - Status: 400, - Err: err, - } -} - -// DNSErr returns a new acme error. -func DNSErr(err error) *Error { - return &Error{ - Type: dnsErr, - Detail: "There was a problem with a DNS query during identifier validation", - Status: 400, - Err: err, - } -} - -// ExternalAccountRequiredErr returns a new acme error. -func ExternalAccountRequiredErr(err error) *Error { - return &Error{ - Type: externalAccountRequiredErr, - Detail: "The request must include a value for the \"externalAccountBinding\" field", - Status: 400, - Err: err, - } -} - -// IncorrectResponseErr returns a new acme error. -func IncorrectResponseErr(err error) *Error { - return &Error{ - Type: incorrectResponseErr, - Detail: "Response received didn't match the challenge's requirements", - Status: 400, - Err: err, - } -} - -// InvalidContactErr returns a new acme error. -func InvalidContactErr(err error) *Error { - return &Error{ - Type: invalidContactErr, - Detail: "A contact URL for an account was invalid", - Status: 400, - Err: err, - } -} - -// MalformedErr returns a new acme error. -func MalformedErr(err error) *Error { - return &Error{ - Type: malformedErr, - Detail: "The request message was malformed", - Status: 400, - Err: err, - } -} - -// OrderNotReadyErr returns a new acme error. -func OrderNotReadyErr(err error) *Error { - return &Error{ - Type: orderNotReadyErr, - Detail: "The request attempted to finalize an order that is not ready to be finalized", - Status: 400, - Err: err, - } -} - -// RateLimitedErr returns a new acme error. -func RateLimitedErr(err error) *Error { - return &Error{ - Type: rateLimitedErr, - Detail: "The request exceeds a rate limit", - Status: 400, - Err: err, - } -} - -// RejectedIdentifierErr returns a new acme error. -func RejectedIdentifierErr(err error) *Error { - return &Error{ - Type: rejectedIdentifierErr, - Detail: "The server will not issue certificates for the identifier", - Status: 400, - Err: err, - } -} - -// ServerInternalErr returns a new acme error. -func ServerInternalErr(err error) *Error { - if err == nil { - return nil - } - return &Error{ - Type: serverInternalErr, - Detail: "The server experienced an internal error", - Status: 500, - Err: err, - } -} - -// NotImplemented returns a new acme error. -func NotImplemented(err error) *Error { - return &Error{ - Type: notImplemented, - Detail: "The requested operation is not implemented", - Status: 501, - Err: err, - } -} - -// TLSErr returns a new acme error. -func TLSErr(err error) *Error { - return &Error{ - Type: tlsErr, - Detail: "The server received a TLS error during validation", - Status: 400, - Err: err, - } -} - -// UnauthorizedErr returns a new acme error. -func UnauthorizedErr(err error) *Error { - return &Error{ - Type: unauthorizedErr, - Detail: "The client lacks sufficient authorization", - Status: 401, - Err: err, - } -} - -// UnsupportedContactErr returns a new acme error. -func UnsupportedContactErr(err error) *Error { - return &Error{ - Type: unsupportedContactErr, - Detail: "A contact URL for an account used an unsupported protocol scheme", - Status: 400, - Err: err, - } -} - -// UnsupportedIdentifierErr returns a new acme error. -func UnsupportedIdentifierErr(err error) *Error { - return &Error{ - Type: unsupportedIdentifierErr, - Detail: "An identifier is of an unsupported type", - Status: 400, - Err: err, - } -} - -// UserActionRequiredErr returns a new acme error. -func UserActionRequiredErr(err error) *Error { - return &Error{ - Type: userActionRequiredErr, - Detail: "Visit the “instance” URL and take actions specified there", - Status: 400, - Err: err, - } -} - -// ProbType is the type of the ACME problem. -type ProbType int +// ProblemType is the type of the ACME problem. +type ProblemType int const ( // The request specified an account that does not exist - accountDoesNotExistErr ProbType = iota + ErrorAccountDoesNotExistType ProblemType = iota // The request specified a certificate to be revoked that has already been revoked - alreadyRevokedErr + ErrorAlreadyRevokedType // The CSR is unacceptable (e.g., due to a short key) - badCSRErr + ErrorBadCSRType // The client sent an unacceptable anti-replay nonce - badNonceErr + ErrorBadNonceType // The JWS was signed by a public key the server does not support - badPublicKeyErr + ErrorBadPublicKeyType // The revocation reason provided is not allowed by the server - badRevocationReasonErr + ErrorBadRevocationReasonType // The JWS was signed with an algorithm the server does not support - badSignatureAlgorithmErr + ErrorBadSignatureAlgorithmType // Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate - caaErr + ErrorCaaType // Specific error conditions are indicated in the “subproblems” array. - compoundErr + ErrorCompoundType // The server could not connect to validation target - connectionErr + ErrorConnectionType // There was a problem with a DNS query during identifier validation - dnsErr + ErrorDNSType // The request must include a value for the “externalAccountBinding” field - externalAccountRequiredErr + ErrorExternalAccountRequiredType // Response received didn’t match the challenge’s requirements - incorrectResponseErr + ErrorIncorrectResponseType // A contact URL for an account was invalid - invalidContactErr + ErrorInvalidContactType // The request message was malformed - malformedErr + ErrorMalformedType // The request attempted to finalize an order that is not ready to be finalized - orderNotReadyErr + ErrorOrderNotReadyType // The request exceeds a rate limit - rateLimitedErr + ErrorRateLimitedType // The server will not issue certificates for the identifier - rejectedIdentifierErr + ErrorRejectedIdentifierType // The server experienced an internal error - serverInternalErr + ErrorServerInternalType // The server received a TLS error during validation - tlsErr + ErrorTLSType // The client lacks sufficient authorization - unauthorizedErr + ErrorUnauthorizedType // A contact URL for an account used an unsupported protocol scheme - unsupportedContactErr + ErrorUnsupportedContactType // An identifier is of an unsupported type - unsupportedIdentifierErr + ErrorUnsupportedIdentifierType // Visit the “instance” URL and take actions specified there - userActionRequiredErr + ErrorUserActionRequiredType // The operation is not implemented - notImplemented + ErrorNotImplementedType ) // String returns the string representation of the acme problem type, // fulfilling the Stringer interface. -func (ap ProbType) String() string { +func (ap ProblemType) String() string { switch ap { - case accountDoesNotExistErr: + case ErrorAccountDoesNotExistType: return "accountDoesNotExist" - case alreadyRevokedErr: + case ErrorAlreadyRevokedType: return "alreadyRevoked" - case badCSRErr: + case ErrorBadCSRType: return "badCSR" - case badNonceErr: + case ErrorBadNonceType: return "badNonce" - case badPublicKeyErr: + case ErrorBadPublicKeyType: return "badPublicKey" - case badRevocationReasonErr: + case ErrorBadRevocationReasonType: return "badRevocationReason" - case badSignatureAlgorithmErr: + case ErrorBadSignatureAlgorithmType: return "badSignatureAlgorithm" - case caaErr: + case ErrorCaaType: return "caa" - case compoundErr: + case ErrorCompoundType: return "compound" - case connectionErr: + case ErrorConnectionType: return "connection" - case dnsErr: + case ErrorDNSType: return "dns" - case externalAccountRequiredErr: + case ErrorExternalAccountRequiredType: return "externalAccountRequired" - case incorrectResponseErr: + case ErrorInvalidContactType: return "incorrectResponse" - case invalidContactErr: + case ErrorInvalidContactType: return "invalidContact" - case malformedErr: + case ErrorMalformedType: return "malformed" - case orderNotReadyErr: + case ErrorOrderNotReadyType: return "orderNotReady" - case rateLimitedErr: + case ErrorRateLimitedType: return "rateLimited" - case rejectedIdentifierErr: + case ErrorRejectedIdentifierType: return "rejectedIdentifier" - case serverInternalErr: + case ErrorServerInternalType: return "serverInternal" - case tlsErr: + case ErrorTLSType: return "tls" - case unauthorizedErr: + case ErrorUnauthorizedType: return "unauthorized" - case unsupportedContactErr: + case ErrorUnsupportedContactType: return "unsupportedContact" - case unsupportedIdentifierErr: + case ErrorUnsupportedIdentifierType: return "unsupportedIdentifier" - case userActionRequiredErr: + case ErrorUserActionRequiredType: return "userActionRequired" - case notImplemented: + case ErrorNotImplementedType: return "notImplemented" default: - return "unsupported type" + return fmt.Sprintf("unsupported type ACME error type %v", ap) + } +} + +type errorMetadata struct { + details string + status int + typ string + String string +} + +var ( + officialACMEPrefix = "urn:ietf:params:acme:error:" + stepACMEPrefix = "urn:step:acme:error:" + errorServerInternalMetadata = errorMetadata{ + ErrorAccountDoesNotExistType: { + typ: officialACMEPrefix + ErrorServerInternalType.String(), + details: "The server experienced an internal error", + status: 500, + }, + } + errorMap = [ProblemType]errorMetadata{ + ErrorAccountDoesNotExistType: { + typ: officialACMEPrefix + ErrorAccountDoesNotExistType.String(), + details: "Account does not exist", + status: 400, + }, + ErrorAlreadyRevokedType: { + typ: officialACMEPrefix + ErrorAlreadyRevokedType.String(), + details: "Certificate already Revoked", + status: 400, + }, + ErrorBadCSRType: { + typ: officialACMEPrefix + ErrorBadCSRType.String(), + details: "The CSR is unacceptable", + status: 400, + }, + ErrorBadNonceType: { + typ: officialACMEPrefix + ErrorBadNonceType.String(), + details: "Unacceptable anti-replay nonce", + status: 400, + }, + ErrorBadPublicKeyType: { + typ: officialACMEPrefix + ErrorBadPublicKeyType.String(), + details: "The jws was signed by a public key the server does not support", + status: 400, + }, + ErrorBadRevocationReasonType: { + typ: officialACMEPrefix + ErrorBadRevocationReasonType.String(), + details: "The revocation reason provided is not allowed by the server", + status: 400, + }, + ErrorBadSignatureAlgorithmType: { + typ: officialACMEPrefix + ErrorBadSignatureAlgorithmType.String(), + details: "The JWS was signed with an algorithm the server does not support", + status: 400, + }, + ErrorCaaType: { + typ: officialACMEPrefix + ErrorCaaType.String(), + details: "Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate", + status: 400, + }, + ErrorCompoundType: { + typ: officialACMEPrefix + ErrorCompoundType.String(), + details: "Specific error conditions are indicated in the “subproblems” array", + status: 400, + }, + ErrorConnectionType: { + typ: officialACMEPrefix + ErrorConnectionType.String(), + details: "The server could not connect to validation target", + status: 400, + }, + ErrorDNSType: { + typ: officialACMEPrefix + ErrorDNSType.String(), + details: "There was a problem with a DNS query during identifier validation", + status: 400, + }, + ErrorExternalAccountRequiredType: { + typ: officialACMEPrefix + ErrorExternalAccountRequiredType.String(), + details: "The request must include a value for the \"externalAccountBinding\" field", + status: 400, + }, + ErrorIncorrectResponseType: { + typ: officialACMEPrefix + ErrorIncorrectResponseType.String(), + details: "Response received didn't match the challenge's requirements", + status: 400, + }, + ErrorInvalidContactType: { + typ: officialACMEPrefix + ErrorInvalidContactType.String(), + details: "A contact URL for an account was invalid", + status: 400, + }, + ErrorMalformedType: { + typ: officialACMEPrefix + ErrorMalformedType.String(), + details: "The request message was malformed", + status: 400, + }, + ErrorOrderNotReadyType: { + typ: officialACMEPrefix + ErrorOrderNotReadyType.String(), + details: "The request attempted to finalize an order that is not ready to be finalized", + status: 400, + }, + ErrorRateLimitedType: { + typ: officialACMEPrefix + ErrorRateLimitedType.String(), + details: "The request exceeds a rate limit", + status: 400, + }, + ErrorRejectedIdentifierType: { + typ: officialACMEPrefix + ErrorRejectedIdentifierType.String(), + details: "The server will not issue certificates for the identifier", + status: 400, + }, + ErrorNotImplementedType: { + typ: officialACMEPrefix + ErrorRejectedIdentifierType.String(), + details: "The requested operation is not implemented", + status: 501, + }, + ErrorTLSType: { + typ: officialACMEPrefix + ErrorTLSType.String(), + details: "The server received a TLS error during validation", + status: 400, + }, + ErrorUnauthorizedType: { + typ: officialACMEPrefix + ErrorUnauthorizedType.String(), + details: "The client lacks sufficient authorization", + status: 401, + }, + ErrorUnsupportedContactType: { + typ: officialACMEPrefix + ErrorUnsupportedContactType.String(), + details: "A contact URL for an account used an unsupported protocol scheme", + status: 400, + }, + ErrorUnsupportedIdentifierType: { + typ: officialACMEPrefix + ErrorUnsupportedIdentifierType.String(), + details: "An identifier is of an unsupported type", + status: 400, + }, + ErrorUserActionRequiredType: { + typ: officialACMEPrefix + ErrorUserActionRequiredType.String(), + details: "Visit the “instance” URL and take actions specified there", + status: 400, + }, + ErrorServerInternalType: errorServerInternalMetadata, } -} +) -// Error is an ACME error type complete with problem document. +// Error represents an ACME type Error struct { - Type ProbType - Detail string - Err error - Status int - Sub []*Error - Identifier *Identifier + Type string `json:"type"` + Detail string `json:"detail"` + Subproblems []interface{} `json:"subproblems,omitempty"` + Identifier interface{} `json:"identifier,omitempty"` + Err error `json:"-"` + Status int `json:"-"` +} + +func NewError(pt ProblemType, msg string, args ...interface{}) *Error { + meta, ok := errorMetadata[typ] + if !ok { + meta = errorServerInternalMetadata + return &Error{ + Type: meta.typ, + Details: meta.details, + Status: meta.Status, + Err: errors.Errorf("unrecognized problemType %v", pt), + } + } + + return &Error{ + Type: meta.typ, + Details: meta.details, + Status: meta.status, + Err: errors.Errorf(msg, args...), + } } -// Wrap attempts to wrap the internal error. -func Wrap(err error, wrap string) *Error { +// ErrorWrap attempts to wrap the internal error. +func ErrorWrap(typ ProblemType, err error, msg string, args ...interface{}) *Error { switch e := err.(type) { case nil: return nil case *Error: if e.Err == nil { - e.Err = errors.New(wrap + "; " + e.Detail) + e.Err = errors.Errorf(msg+"; "+e.Detail, args...) } else { - e.Err = errors.Wrap(e.Err, wrap) + e.Err = errors.Wrapf(e.Err, msg, args...) } return e default: - return ServerInternalErr(errors.Wrap(err, wrap)) + return NewError(ErrorServerInternalType, msg, args...) } } -// Error implements the error interface. +// StatusCode returns the status code and implements the StatusCoder interface. +func (e *Error) StatusCode() int { + return e.Status +} + +// Error allows AError to implement the error interface. func (e *Error) Error() string { - if e.Err == nil { - return e.Detail - } - return e.Err.Error() + return e.Detail } // Cause returns the internal error and implements the Causer interface. @@ -414,71 +328,3 @@ func (e *Error) Cause() error { } return e.Err } - -// Official returns true if this error's type is listed in §6.7 of RFC 8555. -// Error types in §6.7 are registered under IETF urn namespace: -// -// "urn:ietf:params:acme:error:" -// -// and should include the namespace as a prefix when appearing as a problem -// document. -// -// RFC 8555 also says: -// -// This list is not exhaustive. The server MAY return errors whose -// "type" field is set to a URI other than those defined above. Servers -// MUST NOT use the ACME URN namespace for errors not listed in the -// appropriate IANA registry (see Section 9.6). Clients SHOULD display -// the "detail" field of all errors. -// -// In this case Official returns `false` so that a different namespace can -// be used. -func (e *Error) Official() bool { - return e.Type != notImplemented -} - -// ToACME returns an acme representation of the problem type. -// For official errors, the IETF ACME namespace is prepended to the error type. -// For our own errors, we use an (yet) unregistered smallstep acme namespace. -func (e *Error) ToACME() *AError { - prefix := "urn:step:acme:error" - if e.Official() { - prefix = "urn:ietf:params:acme:error:" - } - ae := &AError{ - Type: prefix + e.Type.String(), - Detail: e.Error(), - Status: e.Status, - } - if e.Identifier != nil { - ae.Identifier = *e.Identifier - } - for _, p := range e.Sub { - ae.Subproblems = append(ae.Subproblems, p.ToACME()) - } - return ae -} - -// StatusCode returns the status code and implements the StatusCode interface. -func (e *Error) StatusCode() int { - return e.Status -} - -// AError is the error type as seen in acme request/responses. -type AError struct { - Type string `json:"type"` - Detail string `json:"detail"` - Identifier interface{} `json:"identifier,omitempty"` - Subproblems []interface{} `json:"subproblems,omitempty"` - Status int `json:"-"` -} - -// Error allows AError to implement the error interface. -func (ae *AError) Error() string { - return ae.Detail -} - -// StatusCode returns the status code and implements the StatusCode interface. -func (ae *AError) StatusCode() int { - return ae.Status -} diff --git a/acme/order.go b/acme/order.go index 8879fed0..01d3bc20 100644 --- a/acme/order.go +++ b/acme/order.go @@ -13,18 +13,25 @@ import ( "go.step.sm/crypto/x509util" ) +// Identifier encodes the type that an order pertains to. +type Identifier struct { + Type string `json:"type"` + Value string `json:"value"` +} + // Order contains order metadata for the ACME protocol order type. type Order struct { - Status string `json:"status"` + Status Status `json:"status"` Expires string `json:"expires,omitempty"` Identifiers []Identifier `json:"identifiers"` NotBefore string `json:"notBefore,omitempty"` NotAfter string `json:"notAfter,omitempty"` Error interface{} `json:"error,omitempty"` Authorizations []string `json:"authorizations"` - Finalize string `json:"finalize"` + FinalizeURL string `json:"finalize"` Certificate string `json:"certificate,omitempty"` ID string `json:"-"` + AccountID string `json:"-"` ProvisionerID string `json:"-"` DefaultDuration time.Duration `json:"-"` Backdate time.Duration `json:"-"` @@ -45,7 +52,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { now := time.Now().UTC() expiry, err := time.Parse(time.RFC3339, o.Expires) if err != nil { - return ServerInternalErr(errors.Wrap("error converting expiry string to time")) + return ServerInternalErr(errors.Wrap(err, "order.UpdateStatus - error converting expiry string to time")) } switch o.Status { @@ -69,7 +76,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { break } - var count = map[string]int{ + var count = map[Status]int{ StatusValid: 0, StatusInvalid: 0, StatusPending: 0, @@ -77,10 +84,10 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { for _, azID := range o.Authorizations { az, err := db.GetAuthorization(ctx, azID) if err != nil { - return false, err + return err } - if az, err = az.UpdateStatus(db); err != nil { - return false, err + if err = az.UpdateStatus(ctx, db); err != nil { + return err } st := az.Status count[st]++ @@ -98,20 +105,19 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { o.Status = StatusReady default: - return nil, ServerInternalErr(errors.New("unexpected authz status")) + return ServerInternalErr(errors.New("unexpected authz status")) } default: - return nil, ServerInternalErr(errors.Errorf("unrecognized order status: %s", o.Status)) + return ServerInternalErr(errors.Errorf("unrecognized order status: %s", o.Status)) } return db.UpdateOrder(ctx, o) } -// finalize signs a certificate if the necessary conditions for Order completion +// Finalize signs a certificate if the necessary conditions for Order completion // have been met. -func (o *order) Finalize(ctx, db DB, csr *x509.CertificateRequest, auth SignAuthority, p Provisioner) error { - var err error - if o, err = o.UpdateStatus(db); err != nil { - return nil, err +func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateRequest, auth SignAuthority, p Provisioner) error { + if err := o.UpdateStatus(ctx, db); err != nil { + return err } switch o.Status { @@ -124,7 +130,7 @@ func (o *order) Finalize(ctx, db DB, csr *x509.CertificateRequest, auth SignAuth case StatusReady: break default: - return nil, ServerInternalErr(errors.Errorf("unexpected status %s for order %s", o.Status, o.ID)) + return ServerInternalErr(errors.Errorf("unexpected status %s for order %s", o.Status, o.ID)) } // RFC8555: The CSR MUST indicate the exact same set of requested @@ -135,7 +141,7 @@ func (o *order) Finalize(ctx, db DB, csr *x509.CertificateRequest, auth SignAuth if csr.Subject.CommonName != "" { csr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName) } - csr.DNSNames = uniqueLowerNames(csr.DNSNames) + csr.DNSNames = uniqueSortedLowerNames(csr.DNSNames) orderNames := make([]string, len(o.Identifiers)) for i, n := range o.Identifiers { orderNames[i] = n.Value @@ -148,13 +154,13 @@ func (o *order) Finalize(ctx, db DB, csr *x509.CertificateRequest, auth SignAuth // absence of other SANs as they will only be set if the templates allows // them. if len(csr.DNSNames) != len(orderNames) { - return nil, BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames)) + return BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames)) } sans := make([]x509util.SubjectAlternativeName, len(csr.DNSNames)) for i := range csr.DNSNames { if csr.DNSNames[i] != orderNames[i] { - return nil, BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames)) + return BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames)) } sans[i] = x509util.SubjectAlternativeName{ Type: x509util.DNSType, @@ -163,10 +169,10 @@ func (o *order) Finalize(ctx, db DB, csr *x509.CertificateRequest, auth SignAuth } // Get authorizations from the ACME provisioner. - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) + ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOps, err := p.AuthorizeSign(ctx, "") if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error retrieving authorization options from ACME provisioner")) + return ServerInternalErr(errors.Wrapf(err, "error retrieving authorization options from ACME provisioner")) } // Template data @@ -176,27 +182,36 @@ func (o *order) Finalize(ctx, db DB, csr *x509.CertificateRequest, auth SignAuth templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error creating template options from ACME provisioner")) + return ServerInternalErr(errors.Wrapf(err, "error creating template options from ACME provisioner")) } signOps = append(signOps, templateOptions) - // Create and store a new certificate. + nbf, err := time.Parse(time.RFC3339, o.NotBefore) + if err != nil { + return ServerInternalErr(errors.Wrap(err, "error parsing order NotBefore")) + } + naf, err := time.Parse(time.RFC3339, o.NotAfter) + if err != nil { + return ServerInternalErr(errors.Wrap(err, "error parsing order NotAfter")) + } + + // Sign a new certificate. certChain, err := auth.Sign(csr, provisioner.SignOptions{ - NotBefore: provisioner.NewTimeDuration(o.NotBefore), - NotAfter: provisioner.NewTimeDuration(o.NotAfter), + NotBefore: provisioner.NewTimeDuration(nbf), + NotAfter: provisioner.NewTimeDuration(naf), }, signOps...) if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error generating certificate for order %s", o.ID)) + return ServerInternalErr(errors.Wrapf(err, "error signing certificate for order %s", o.ID)) } - cert, err := db.CreateCertificate(ctx, &Certificate{ + cert := &Certificate{ AccountID: o.AccountID, OrderID: o.ID, Leaf: certChain[0], Intermediates: certChain[1:], - }) - if err != nil { - return nil, err + } + if err := db.CreateCertificate(ctx, cert); err != nil { + return err } o.Certificate = cert.ID diff --git a/authority/provisioner/method.go b/authority/provisioner/method.go index 775ed96f..f5cd5221 100644 --- a/authority/provisioner/method.go +++ b/authority/provisioner/method.go @@ -56,8 +56,7 @@ func NewContextWithMethod(ctx context.Context, method Method) context.Context { return context.WithValue(ctx, methodKey{}, method) } -// MethodFromContext returns the Method saved in ctx. Returns Sign if the given -// context has no Method associated with it. +// MethodFromContext returns the Method saved in ctx. func MethodFromContext(ctx context.Context) Method { m, _ := ctx.Value(methodKey{}).(Method) return m