From 1135ae04fc97fa21a3c47be211df21aff8c846fc Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 3 Mar 2021 15:16:25 -0800 Subject: [PATCH] [acme db interface] wip --- acme/api/account.go | 22 ++--- acme/api/order.go | 4 +- acme/authority.go | 169 +++++++++++++++++++++++++++++++------ acme/authorization.go | 22 ++--- acme/challenge.go | 14 +-- acme/db/nosql/authz.go | 44 ++-------- acme/db/nosql/challenge.go | 18 ---- acme/db/nosql/order.go | 44 +--------- acme/directory.go | 2 +- acme/errors.go | 63 +++++++------- acme/order.go | 68 ++++++--------- api/errors.go | 3 +- 12 files changed, 251 insertions(+), 222 deletions(-) diff --git a/acme/api/account.go b/acme/api/account.go index ec2854cc..5e208a5f 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -21,7 +21,7 @@ type NewAccountRequest struct { func validateContacts(cs []string) error { for _, c := range cs { if len(c) == 0 { - return acme.MalformedErr(errors.New("contact cannot be empty string")) + return acme.NewError(acme.ErrorMalformedType, "contact cannot be empty string") } } return nil @@ -30,7 +30,7 @@ func validateContacts(cs []string) error { // Validate validates a new-account request body. func (n *NewAccountRequest) Validate() error { if n.OnlyReturnExisting && len(n.Contact) > 0 { - return acme.MalformedErr(errors.New("incompatible input; onlyReturnExisting must be alone")) + return acme.NewError(acme.ErrorMalformedType, "incompatible input; onlyReturnExisting must be alone") } return validateContacts(n.Contact) } @@ -51,8 +51,8 @@ func (u *UpdateAccountRequest) IsDeactivateRequest() bool { func (u *UpdateAccountRequest) Validate() error { switch { case len(u.Status) > 0 && len(u.Contact) > 0: - return acme.MalformedErr(errors.New("incompatible input; contact and " + - "status updates are mutually exclusive")) + return acme.NewError(acme.ErrorMalformedType, "incompatible input; contact and "+ + "status updates are mutually exclusive") case len(u.Contact) > 0: if err := validateContacts(u.Contact); err != nil { return err @@ -60,8 +60,8 @@ func (u *UpdateAccountRequest) Validate() error { return nil case len(u.Status) > 0: if u.Status != string(acme.StatusDeactivated) { - return acme.MalformedErr(errors.Errorf("cannot update account "+ - "status to %s, only deactivated", u.Status)) + return acme.NewError(acme.ErrorMalformedType, "cannot update account "+ + "status to %s, only deactivated", u.Status) } return nil default: @@ -80,8 +80,8 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { } var nar NewAccountRequest if err := json.Unmarshal(payload.value, &nar); err != nil { - api.WriteError(w, acme.MalformedErr(errors.Wrap(err, - "failed to unmarshal new-account request payload"))) + api.WriteError(w, acme.ErrorWrap(acme.ErrorMalformedType, err, + "failed to unmarshal new-account request payload")) return } if err := nar.Validate(); err != nil { @@ -101,7 +101,8 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { // Account does not exist // if nar.OnlyReturnExisting { - api.WriteError(w, acme.AccountDoesNotExistErr(nil)) + api.WriteError(w, acme.NewError(acme.ErrorAccountDoesNotExistType, + "account does not exist")) return } jwk, err := acme.JwkFromContext(r.Context()) @@ -146,7 +147,8 @@ func (h *Handler) GetUpdateAccount(w http.ResponseWriter, r *http.Request) { if !payload.isPostAsGet { var uar UpdateAccountRequest if err := json.Unmarshal(payload.value, &uar); err != nil { - api.WriteError(w, acme.MalformedErr(errors.Wrap(err, "failed to unmarshal new-account request payload"))) + api.WriteError(w, acme.ErrorWrap(acme.ErrorMalformedType, err, + "failed to unmarshal new-account request payload")) return } if err := uar.Validate(); err != nil { diff --git a/acme/api/order.go b/acme/api/order.go index 5c62cb52..1fead85c 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -23,11 +23,11 @@ type NewOrderRequest struct { // Validate validates a new-order request body. func (n *NewOrderRequest) Validate() error { if len(n.Identifiers) == 0 { - return acme.MalformedErr(errors.Errorf("identifiers list cannot be empty")) + return acme.NewError(ErrorMalformedType, "identifiers list cannot be empty") } for _, id := range n.Identifiers { if id.Type != "dns" { - return acme.MalformedErr(errors.Errorf("identifier type unsupported: %s", id.Type)) + return acme.NewError(ErrorMalformedType, "identifier type unsupported: %s", id.Type) } } return nil diff --git a/acme/authority.go b/acme/authority.go index 098c48d4..92e1c8f7 100644 --- a/acme/authority.go +++ b/acme/authority.go @@ -8,10 +8,12 @@ import ( "net" "net/http" "net/url" + "strings" "time" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" + "go.step.sm/crypto/randutil" ) // Interface is the acme authority interface. @@ -124,7 +126,7 @@ 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, acc *Account) error { if err := a.db.CreateAccount(ctx, acc); err != nil { - return ErrorWrap(ErrorServerInternalType, err, "error creating account") + return ErrorISEWrap(err, "error creating account") } return nil } @@ -136,7 +138,7 @@ func (a *Authority) UpdateAccount(ctx context.Context, acc *Account) (*Account, acc.Status = auo.Status */ if err := a.db.UpdateAccount(ctx, acc); err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error updating account") + return nil, ErrorISEWrap(err, "error updating account") } return acc, nil } @@ -145,7 +147,7 @@ func (a *Authority) UpdateAccount(ctx context.Context, acc *Account) (*Account, func (a *Authority) GetAccount(ctx context.Context, id string) (*Account, error) { acc, err := a.db.GetAccount(ctx, id) if err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving account") + return nil, ErrorISEWrap(err, "error retrieving account") } return acc, nil } @@ -168,7 +170,7 @@ func (a *Authority) GetOrder(ctx context.Context, accID, orderID string) (*Order } o, err := a.db.GetOrder(ctx, orderID) if err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving order") + return nil, ErrorISEWrap(err, "error retrieving order") } if accID != o.AccountID { log.Printf("account-id from request ('%s') does not match order account-id ('%s')", accID, o.AccountID) @@ -179,7 +181,7 @@ func (a *Authority) GetOrder(ctx context.Context, accID, orderID string) (*Order return nil, NewError(ErrorUnauthorizedType, "provisioner does not own order") } if err = o.UpdateStatus(ctx, a.db); err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error updating order") + return nil, ErrorISEWrap(err, "error updating order") } return o, nil } @@ -205,19 +207,54 @@ 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, o *Order) (*Order, error) { - prov, err := ProvisionerFromContext(ctx) - if err != nil { - return nil, err +func (a *Authority) NewOrder(ctx context.Context, o *Order) error { + if len(o.AccountID) == 0 { + return NewErrorISE("account-id cannot be empty") + } + if len(o.ProvisionerID) == 0 { + return NewErrorISE("provisioner-id cannot be empty") + } + if len(o.Identifiers) == 0 { + return NewErrorISE("identifiers cannot be empty") + } + if o.DefaultDuration == 0 { + return NewErrorISE("default-duration cannot be empty") } - o.DefaultDuration = prov.DefaultTLSCertDuration() - o.Backdate = a.backdate.Duration - o.ProvisionerID = prov.GetID() - if err = a.db.CreateOrder(ctx, o); err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error creating order") + o.AuthorizationIDs = make([]string, len(o.Identifiers)) + for i, identifier := range o.Identifiers { + az := &Authorization{ + AccountID: o.AccountID, + Identifier: identifier, + } + if err := a.NewAuthorization(ctx, az); err != nil { + return err + } + o.AuthorizationIDs[i] = az.ID } - return o, nil + + now := clock.Now() + if o.NotBefore.IsZero() { + o.NotBefore = now + } + if o.NotAfter.IsZero() { + o.NotAfter = o.NotBefore.Add(o.DefaultDuration) + } + + if err := a.db.CreateOrder(ctx, o); err != nil { + return ErrorISEWrap(err, "error creating order") + } + return nil + /* + o.DefaultDuration = prov.DefaultTLSCertDuration() + o.Backdate = a.backdate.Duration + o.ProvisionerID = prov.GetID() + + if err = a.db.CreateOrder(ctx, o); err != nil { + return nil, ErrorWrap(ErrorServerInternalType, err, "error creating order") + } + return o, nil + */ } // FinalizeOrder attempts to finalize an order and generate a new certificate. @@ -228,7 +265,7 @@ func (a *Authority) FinalizeOrder(ctx context.Context, accID, orderID string, cs } o, err := a.db.GetOrder(ctx, orderID) if err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving order") + return nil, ErrorISEWrap(err, "error retrieving order") } if accID != o.AccountID { log.Printf("account-id from request ('%s') does not match order account-id ('%s')", accID, o.AccountID) @@ -239,33 +276,113 @@ func (a *Authority) FinalizeOrder(ctx context.Context, accID, orderID string, cs return nil, NewError(ErrorUnauthorizedType, "provisioner does not own order") } if err = o.Finalize(ctx, a.db, csr, a.signAuth, prov); err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error finalizing order") + return nil, ErrorISEWrap(err, "error finalizing order") } return o, nil } -// GetAuthz retrieves and attempts to update the status on an ACME authz +// NewAuthorization generates and stores an ACME Authorization type along with +// any associated resources. +func (a *Authority) NewAuthorization(ctx context.Context, az *Authorization) error { + if len(az.AccountID) == 0 { + return NewErrorISE("account-id cannot be empty") + } + if len(az.Identifier.Value) == 0 { + return NewErrorISE("identifier cannot be empty") + } + + if strings.HasPrefix(az.Identifier.Value, "*.") { + az.Wildcard = true + az.Identifier = Identifier{ + Value: strings.TrimPrefix(az.Identifier.Value, "*."), + Type: az.Identifier.Type, + } + } + + var ( + err error + chTypes = []string{"dns-01"} + ) + // HTTP and TLS challenges can only be used for identifiers without wildcards. + if !az.Wildcard { + chTypes = append(chTypes, []string{"http-01", "tls-alpn-01"}...) + } + + az.Token, err = randutil.Alphanumeric(32) + if err != nil { + return ErrorISEWrap(err, "error generating random alphanumeric ID") + } + + az.Challenges = make([]*Challenge, len(chTypes)) + for i, typ := range chTypes { + ch := &Challenge{ + AccountID: az.AccountID, + AuthzID: az.ID, + Value: az.Identifier.Value, + Type: typ, + Token: az.Token, + } + if err := a.NewChallenge(ctx, ch); err != nil { + return err + } + az.Challenges[i] = ch + } + if err = a.db.CreateAuthorization(ctx, az); err != nil { + return ErrorISEWrap(err, "error creating authorization") + } + return nil +} + +// GetAuthorization retrieves and attempts to update the status on an ACME authz // before returning. -func (a *Authority) GetAuthz(ctx context.Context, accID, authzID string) (*Authorization, error) { +func (a *Authority) GetAuthorization(ctx context.Context, accID, authzID string) (*Authorization, error) { az, err := a.db.GetAuthorization(ctx, authzID) if err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving authorization") + return nil, ErrorISEWrap(err, "error retrieving authorization") } if accID != az.AccountID { log.Printf("account-id from request ('%s') does not match authz account-id ('%s')", accID, az.AccountID) return nil, NewError(ErrorUnauthorizedType, "account does not own order") } if err = az.UpdateStatus(ctx, a.db); err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error updating authorization status") + return nil, ErrorISEWrap(err, "error updating authorization status") } return az, nil } -// ValidateChallenge attempts to validate the challenge. -func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, jwk *jose.JSONWebKey) (*Challenge, error) { +// NewChallenge generates and stores an ACME challenge and associated resources. +func (a *Authority) NewChallenge(ctx context.Context, ch *Challenge) error { + if len(ch.AccountID) == 0 { + return NewErrorISE("account-id cannot be empty") + } + if len(ch.AuthzID) == 0 { + return NewErrorISE("authz-id cannot be empty") + } + if len(ch.Token) == 0 { + return NewErrorISE("token cannot be empty") + } + if len(ch.Value) == 0 { + return NewErrorISE("value cannot be empty") + } + + switch ch.Type { + case "dns-01", "http-01", "tls-alpn-01": + break + default: + return NewErrorISE("unexpected error type '%s'", ch.Type) + } + + if err := a.db.CreateChallenge(ctx, ch); err != nil { + return ErrorISEWrap(err, "error creating challenge") + } + return nil +} + +// GetValidateChallenge attempts to validate the challenge. +func (a *Authority) GetValidateChallenge(ctx context.Context, accID, chID, azID string, jwk *jose.JSONWebKey) (*Challenge, error) { ch, err := a.db.GetChallenge(ctx, chID, "todo") if err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving challenge") + return nil, ErrorISEWrap(err, "error retrieving challenge") } if accID != ch.AccountID { log.Printf("account-id from request ('%s') does not match challenge account-id ('%s')", accID, ch.AccountID) @@ -284,7 +401,7 @@ func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, j return tls.DialWithDialer(dialer, network, addr, config) }, }); err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error validating challenge") + return nil, ErrorISEWrap(err, "error validating challenge") } return ch, nil } @@ -293,7 +410,7 @@ func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, j func (a *Authority) GetCertificate(ctx context.Context, accID, certID string) ([]byte, error) { cert, err := a.db.GetCertificate(ctx, certID) if err != nil { - return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving certificate") + return nil, ErrorISEWrap(err, "error retrieving certificate") } if cert.AccountID != accID { log.Printf("account-id from request ('%s') does not match challenge account-id ('%s')", accID, cert.AccountID) diff --git a/acme/authorization.go b/acme/authorization.go index ef230286..e4bc669d 100644 --- a/acme/authorization.go +++ b/acme/authorization.go @@ -8,20 +8,22 @@ import ( // Authorization representst an ACME Authorization. type Authorization struct { - Identifier *Identifier `json:"identifier"` - Status Status `json:"status"` - Expires string `json:"expires"` - Challenges []*Challenge `json:"challenges"` - Wildcard bool `json:"wildcard"` - ID string `json:"-"` - AccountID string `json:"-"` + Identifier Identifier `json:"identifier"` + Status Status `json:"status"` + Expires string `json:"expires"` + Challenges []*Challenge `json:"challenges"` + ChallengeIDs string `json::"-"` + Wildcard bool `json:"wildcard"` + ID string `json:"-"` + AccountID string `json:"-"` + Token string `json:"-"` } // ToLog enables response logging. func (az *Authorization) ToLog() (interface{}, error) { b, err := json.Marshal(az) if err != nil { - return nil, ErrorInternalServerWrap(err, "error marshaling authz for logging") + return nil, ErrorISEWrap(err, "error marshaling authz for logging") } return string(b), nil } @@ -32,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 ErrorInternalServerWrap(err, "error converting expiry string to time") + return ErrorISEWrap(err, "error converting expiry string to time") } switch az.Status { @@ -64,7 +66,7 @@ func (az *Authorization) UpdateStatus(ctx context.Context, db DB) error { } if err = db.UpdateAuthorization(ctx, az); err != nil { - return ErrorInternalServerWrap(err, "error updating authorization") + return ErrorISEWrap(err, "error updating authorization") } return nil } diff --git a/acme/challenge.go b/acme/challenge.go index 05987427..ca2e5562 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -38,7 +38,7 @@ type Challenge struct { func (ch *Challenge) ToLog() (interface{}, error) { b, err := json.Marshal(ch) if err != nil { - return nil, ErrorInternalServerWrap(err, "error marshaling challenge for logging") + return nil, ErrorISEWrap(err, "error marshaling challenge for logging") } return string(b), nil } @@ -80,7 +80,7 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb body, err := ioutil.ReadAll(resp.Body) if err != nil { - return ErrorInternalServerWrap(err, "error reading "+ + return ErrorISEWrap(err, "error reading "+ "response body for url %s", url) } keyAuth := strings.Trim(string(body), "\r\n") @@ -100,7 +100,7 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb ch.Validated = clock.Now().Format(time.RFC3339) if err = db.UpdateChallenge(ctx, ch); err != nil { - return ErrorInternalServerWrap(err, "error updating challenge") + return ErrorISEWrap(err, "error updating challenge") } return nil } @@ -178,7 +178,7 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON ch.Validated = clock.Now().Format(time.RFC3339) if err = db.UpdateChallenge(ctx, ch); err != nil { - return ErrorInternalServerWrap(err, "tlsalpn01ValidateChallenge - error updating challenge") + return ErrorISEWrap(err, "tlsalpn01ValidateChallenge - error updating challenge") } return nil } @@ -234,7 +234,7 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK ch.Validated = clock.Now().UTC().Format(time.RFC3339) if err = db.UpdateChallenge(ctx, ch); err != nil { - return ErrorInternalServerWrap(err, "error updating challenge") + return ErrorISEWrap(err, "error updating challenge") } return nil } @@ -244,7 +244,7 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) { thumbprint, err := jwk.Thumbprint(crypto.SHA256) if err != nil { - return "", ErrorInternalServerWrap(err, "error generating JWK thumbprint") + return "", ErrorISEWrap(err, "error generating JWK thumbprint") } encPrint := base64.RawURLEncoding.EncodeToString(thumbprint) return fmt.Sprintf("%s.%s", token, encPrint), nil @@ -254,7 +254,7 @@ func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) { func storeError(ctx context.Context, ch *Challenge, db DB, err *Error) error { ch.Error = err if err := db.UpdateChallenge(ctx, ch); err != nil { - return ErrorInternalServerWrap(err, "failure saving error to acme challenge") + return ErrorISEWrap(err, "failure saving error to acme challenge") } return nil } diff --git a/acme/db/nosql/authz.go b/acme/db/nosql/authz.go index bc9f75bc..818f5c2d 100644 --- a/acme/db/nosql/authz.go +++ b/acme/db/nosql/authz.go @@ -3,7 +3,6 @@ package nosql import ( "context" "encoding/json" - "strings" "time" "github.com/pkg/errors" @@ -75,18 +74,17 @@ func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorizat // CreateAuthorization creates an entry in the database for the Authorization. // Implements the acme.DB.CreateAuthorization interface. func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) error { - if len(az.AccountID) == 0 { - return errors.New("account-id cannot be empty") - } - if az.Identifier == nil { - return errors.New("identifier cannot be nil") - } var err error az.ID, err = randID() if err != nil { return err } + chIDs := make([]string, len(az.Challenges)) + for i, ch := range az.Challenges { + chIDs[i] = ch.ID + } + now := clock.Now() dbaz := &dbAuthz{ ID: az.ID, @@ -95,38 +93,10 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) e Created: now, Expires: now.Add(defaultExpiryDuration), Identifier: az.Identifier, + Challenges: chIDs, + Wildcard: az.Wildcard, } - if strings.HasPrefix(az.Identifier.Value, "*.") { - dbaz.Wildcard = true - dbaz.Identifier = &acme.Identifier{ - Value: strings.TrimPrefix(az.Identifier.Value, "*."), - Type: az.Identifier.Type, - } - } - - chIDs := []string{} - chTypes := []string{"dns-01"} - // HTTP and TLS challenges can only be used for identifiers without wildcards. - if !dbaz.Wildcard { - chTypes = append(chTypes, []string{"http-01", "tls-alpn-01"}...) - } - - for _, typ := range chTypes { - ch := &acme.Challenge{ - AccountID: az.AccountID, - AuthzID: az.ID, - Value: az.Identifier.Value, - Type: typ, - } - if err = db.CreateChallenge(ctx, ch); err != nil { - return errors.Wrapf(err, "error creating challenge") - } - - chIDs = append(chIDs, ch.ID) - } - dbaz.Challenges = chIDs - return db.save(ctx, az.ID, dbaz, nil, "authz", authzTable) } diff --git a/acme/db/nosql/challenge.go b/acme/db/nosql/challenge.go index 62513778..378b1f7b 100644 --- a/acme/db/nosql/challenge.go +++ b/acme/db/nosql/challenge.go @@ -47,29 +47,11 @@ func (db *DB) getDBChallenge(ctx context.Context, id string) (*dbChallenge, erro // CreateChallenge creates a new ACME challenge data structure in the database. // Implements acme.DB.CreateChallenge interface. func (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error { - if len(ch.AuthzID) == 0 { - return errors.New("AuthzID cannot be empty") - } - if len(ch.AccountID) == 0 { - return errors.New("AccountID cannot be empty") - } - if len(ch.Value) == 0 { - 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 errors.New("Type cannot be empty") - } - var err error ch.ID, err = randID() if err != nil { return errors.Wrap(err, "error generating random id for ACME challenge") } - ch.Token, err = randID() - if err != nil { - return errors.Wrap(err, "error generating token for ACME challenge") - } dbch := &dbChallenge{ ID: ch.ID, diff --git a/acme/db/nosql/order.go b/acme/db/nosql/order.go index 528619d4..d2146e22 100644 --- a/acme/db/nosql/order.go +++ b/acme/db/nosql/order.go @@ -74,48 +74,12 @@ func (db *DB) GetOrder(ctx context.Context, id string) (*acme.Order, error) { // CreateOrder creates ACME Order resources and saves them to the DB. func (db *DB) CreateOrder(ctx context.Context, o *acme.Order) error { - if len(o.AccountID) == 0 { - return ServerInternalErr(errors.New("account-id cannot be empty")) - } - if len(o.ProvisionerID) == 0 { - return ServerInternalErr(errors.New("provisioner-id cannot be empty")) - } - if len(o.Identifiers) == 0 { - return ServerInternalErr(errors.New("identifiers cannot be empty")) - } - if o.DefaultDuration == 0 { - return ServerInternalErr(errors.New("default-duration cannot be empty")) - } - o.ID, err = randID() if err != nil { return nil, err } - azIDs := make([]string, len(ops.Identifiers)) - for i, identifier := range ops.Identifiers { - az, err = db.CreateAuthorzation(&types.Authorization{ - AccountID: o.AccountID, - Identifier: o.Identifier, - }) - if err != nil { - return err - } - azIDs[i] = az.ID - } - now := clock.Now() - var backdate time.Duration - nbf := o.NotBefore - if nbf.IsZero() { - nbf = now - backdate = -1 * o.Backdate - } - naf := o.NotAfter - if naf.IsZero() { - naf = nbf.Add(o.DefaultDuration) - } - dbo := &dbOrder{ ID: o.ID, AccountID: o.AccountID, @@ -123,10 +87,10 @@ func (db *DB) CreateOrder(ctx context.Context, o *acme.Order) error { Created: now, Status: StatusPending, Expires: now.Add(defaultOrderExpiry), - Identifiers: ops.Identifiers, - NotBefore: nbf.Add(backdate), - NotAfter: naf, - Authorizations: azIDs, + Identifiers: o.Identifiers, + NotBefore: o.NotBefore, + NotAfter: o.NotBefore, + Authorizations: o.AuthorizationIDs, } if err := db.save(ctx, o.ID, dbo, nil, orderTable); err != nil { return nil, err diff --git a/acme/directory.go b/acme/directory.go index 1b5b8c4b..8520d0e9 100644 --- a/acme/directory.go +++ b/acme/directory.go @@ -21,7 +21,7 @@ type Directory struct { func (d *Directory) ToLog() (interface{}, error) { b, err := json.Marshal(d) if err != nil { - return nil, ErrorInternalServerWrap(err, "error marshaling directory for logging") + return nil, ErrorISEWrap(err, "error marshaling directory for logging") } return string(b), nil } diff --git a/acme/errors.go b/acme/errors.go index aabc7302..8fe2559d 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -1,4 +1,3 @@ -// Error represents an ACME package acme import ( @@ -11,55 +10,55 @@ import ( type ProblemType int const ( - // The request specified an account that does not exist + // ErrorAccountDoesNotExistType request specified an account that does not exist ErrorAccountDoesNotExistType ProblemType = iota - // The request specified a certificate to be revoked that has already been revoked + // ErrorAlreadyRevokedType request specified a certificate to be revoked that has already been revoked ErrorAlreadyRevokedType - // The CSR is unacceptable (e.g., due to a short key) + // ErrorBadCSRType CSR is unacceptable (e.g., due to a short key) ErrorBadCSRType - // The client sent an unacceptable anti-replay nonce + // ErrorBadNonceType client sent an unacceptable anti-replay nonce ErrorBadNonceType - // The JWS was signed by a public key the server does not support + // ErrorBadPublicKeyType JWS was signed by a public key the server does not support ErrorBadPublicKeyType - // The revocation reason provided is not allowed by the server + // ErrorBadRevocationReasonType revocation reason provided is not allowed by the server ErrorBadRevocationReasonType - // The JWS was signed with an algorithm the server does not support + // ErrorBadSignatureAlgorithmType JWS was signed with an algorithm the server does not support ErrorBadSignatureAlgorithmType - // Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate + // ErrorCaaType Authority Authorization (CAA) records forbid the CA from issuing a certificate ErrorCaaType - // Specific error conditions are indicated in the “subproblems” array. + // ErrorCompoundType error conditions are indicated in the “subproblems” array. ErrorCompoundType - // The server could not connect to validation target + // ErrorConnectionType server could not connect to validation target ErrorConnectionType - // There was a problem with a DNS query during identifier validation + // ErrorDNSType was a problem with a DNS query during identifier validation ErrorDNSType - // The request must include a value for the “externalAccountBinding” field + // ErrorExternalAccountRequiredType request must include a value for the “externalAccountBinding” field ErrorExternalAccountRequiredType - // Response received didn’t match the challenge’s requirements + // ErrorIncorrectResponseType received didn’t match the challenge’s requirements ErrorIncorrectResponseType - // A contact URL for an account was invalid + // ErrorInvalidContactType URL for an account was invalid ErrorInvalidContactType - // The request message was malformed + // ErrorMalformedType request message was malformed ErrorMalformedType - // The request attempted to finalize an order that is not ready to be finalized + // ErrorOrderNotReadyType request attempted to finalize an order that is not ready to be finalized ErrorOrderNotReadyType - // The request exceeds a rate limit + // ErrorRateLimitedType request exceeds a rate limit ErrorRateLimitedType - // The server will not issue certificates for the identifier + // ErrorRejectedIdentifierType server will not issue certificates for the identifier ErrorRejectedIdentifierType - // The server experienced an internal error + // ErrorServerInternalType server experienced an internal error ErrorServerInternalType - // The server received a TLS error during validation + // ErrorTLSType server received a TLS error during validation ErrorTLSType - // The client lacks sufficient authorization + // ErrorUnauthorizedType client lacks sufficient authorization ErrorUnauthorizedType - // A contact URL for an account used an unsupported protocol scheme + // ErrorUnsupportedContactType URL for an account used an unsupported protocol scheme ErrorUnsupportedContactType - // An identifier is of an unsupported type + // ErrorUnsupportedIdentifierType identifier is of an unsupported type ErrorUnsupportedIdentifierType - // Visit the “instance” URL and take actions specified there + // ErrorUserActionRequiredType the “instance” URL and take actions specified there ErrorUserActionRequiredType - // The operation is not implemented + // ErrorNotImplementedType operation is not implemented ErrorNotImplementedType ) @@ -116,7 +115,7 @@ func (ap ProblemType) String() string { case ErrorNotImplementedType: return "notImplemented" default: - return fmt.Sprintf("unsupported type ACME error type %v", ap) + return fmt.Sprintf("unsupported type ACME error type '%d'", int(ap)) } } @@ -270,6 +269,7 @@ type Error struct { Status int `json:"-"` } +// NewError creates a new Error type. func NewError(pt ProblemType, msg string, args ...interface{}) *Error { meta, ok := errorMap[pt] if !ok { @@ -290,6 +290,11 @@ func NewError(pt ProblemType, msg string, args ...interface{}) *Error { } } +// NewErrorISE creates a new ErrorServerInternalType Error. +func NewErrorISE(msg string, args ...interface{}) *Error { + return NewError(ErrorServerInternalType, msg, args...) +} + // ErrorWrap attempts to wrap the internal error. func ErrorWrap(typ ProblemType, err error, msg string, args ...interface{}) *Error { switch e := err.(type) { @@ -307,8 +312,8 @@ func ErrorWrap(typ ProblemType, err error, msg string, args ...interface{}) *Err } } -// ErrorInternalServerWrap shortcut to wrap an internal server error type. -func ErrorInternalServerWrap(err error, msg string, args ...interface{}) *Error { +// ErrorISEWrap shortcut to wrap an internal server error type. +func ErrorISEWrap(err error, msg string, args ...interface{}) *Error { return ErrorWrap(ErrorServerInternalType, err, msg, args...) } diff --git a/acme/order.go b/acme/order.go index e0ac822b..bf3297f9 100644 --- a/acme/order.go +++ b/acme/order.go @@ -20,27 +20,28 @@ type Identifier struct { // Order contains order metadata for the ACME protocol order type. type Order struct { - 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"` - 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:"-"` + Status Status `json:"status"` + Expires time.Time `json:"expires,omitempty"` + Identifiers []Identifier `json:"identifiers"` + NotBefore time.Time `json:"notBefore,omitempty"` + NotAfter time.Time `json:"notAfter,omitempty"` + Error interface{} `json:"error,omitempty"` + AuthorizationURLs []string `json:"authorizations"` + AuthorizationIDs []string `json:"-"` + 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:"-"` } // ToLog enables response logging. func (o *Order) ToLog() (interface{}, error) { b, err := json.Marshal(o) if err != nil { - return nil, ErrorInternalServerWrap(err, "error marshaling order for logging") + return nil, ErrorISEWrap(err, "error marshaling order for logging") } return string(b), nil } @@ -49,10 +50,6 @@ func (o *Order) ToLog() (interface{}, error) { // Changes to the order are saved using the database interface. 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 ErrorInternalServerWrap(err, "order.UpdateStatus - error converting expiry string to time") - } switch o.Status { case StatusInvalid: @@ -61,7 +58,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { return nil case StatusReady: // Check expiry - if now.After(expiry) { + if now.After(o.Expires) { o.Status = StatusInvalid o.Error = NewError(ErrorMalformedType, "order has expired") break @@ -69,7 +66,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { return nil case StatusPending: // Check expiry - if now.After(expiry) { + if now.After(o.Expires) { o.Status = StatusInvalid o.Error = NewError(ErrorMalformedType, "order has expired") break @@ -80,7 +77,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { StatusInvalid: 0, StatusPending: 0, } - for _, azID := range o.Authorizations { + for _, azID := range o.AuthorizationIDs { az, err := db.GetAuthorization(ctx, azID) if err != nil { return err @@ -100,14 +97,14 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { case count[StatusPending] > 0: return nil - case count[StatusValid] == len(o.Authorizations): + case count[StatusValid] == len(o.AuthorizationIDs): o.Status = StatusReady default: - return NewError(ErrorServerInternalType, "unexpected authz status") + return NewErrorISE("unexpected authz status") } default: - return NewError(ErrorServerInternalType, "unrecognized order status: %s", o.Status) + return NewErrorISE("unrecognized order status: %s", o.Status) } return db.UpdateOrder(ctx, o) } @@ -129,7 +126,7 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques case StatusReady: break default: - return NewError(ErrorServerInternalType, "unexpected status %s for order %s", o.Status, o.ID) + return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) } // RFC8555: The CSR MUST indicate the exact same set of requested @@ -173,7 +170,7 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOps, err := p.AuthorizeSign(ctx, "") if err != nil { - return ErrorInternalServerWrap(err, "error retrieving authorization options from ACME provisioner") + return ErrorISEWrap(err, "error retrieving authorization options from ACME provisioner") } // Template data @@ -183,26 +180,17 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) if err != nil { - return ErrorInternalServerWrap(err, "error creating template options from ACME provisioner") + return ErrorISEWrap(err, "error creating template options from ACME provisioner") } signOps = append(signOps, templateOptions) - nbf, err := time.Parse(time.RFC3339, o.NotBefore) - if err != nil { - return ErrorInternalServerWrap(err, "error parsing order NotBefore") - } - naf, err := time.Parse(time.RFC3339, o.NotAfter) - if err != nil { - return ErrorInternalServerWrap(err, "error parsing order NotAfter") - } - // Sign a new certificate. certChain, err := auth.Sign(csr, provisioner.SignOptions{ - NotBefore: provisioner.NewTimeDuration(nbf), - NotAfter: provisioner.NewTimeDuration(naf), + NotBefore: provisioner.NewTimeDuration(o.NotBefore), + NotAfter: provisioner.NewTimeDuration(o.NotAfter), }, signOps...) if err != nil { - return ErrorInternalServerWrap(err, "error signing certificate for order %s", o.ID) + return ErrorISEWrap(err, "error signing certificate for order %s", o.ID) } cert := &Certificate{ diff --git a/api/errors.go b/api/errors.go index 93057ed2..460192fc 100644 --- a/api/errors.go +++ b/api/errors.go @@ -14,10 +14,9 @@ import ( // WriteError writes to w a JSON representation of the given error. func WriteError(w http.ResponseWriter, err error) { - switch k := err.(type) { + switch err.(type) { case *acme.Error: w.Header().Set("Content-Type", "application/problem+json") - err = k.ToACME() default: w.Header().Set("Content-Type", "application/json") }