[acme db interface] wip

This commit is contained in:
max furman 2021-03-03 15:16:25 -08:00
parent 03ba229bcb
commit 1135ae04fc
12 changed files with 251 additions and 222 deletions

View file

@ -21,7 +21,7 @@ type NewAccountRequest struct {
func validateContacts(cs []string) error { func validateContacts(cs []string) error {
for _, c := range cs { for _, c := range cs {
if len(c) == 0 { 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 return nil
@ -30,7 +30,7 @@ func validateContacts(cs []string) error {
// Validate validates a new-account request body. // Validate validates a new-account request body.
func (n *NewAccountRequest) Validate() error { func (n *NewAccountRequest) Validate() error {
if n.OnlyReturnExisting && len(n.Contact) > 0 { 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) return validateContacts(n.Contact)
} }
@ -51,8 +51,8 @@ func (u *UpdateAccountRequest) IsDeactivateRequest() bool {
func (u *UpdateAccountRequest) Validate() error { func (u *UpdateAccountRequest) Validate() error {
switch { switch {
case len(u.Status) > 0 && len(u.Contact) > 0: case len(u.Status) > 0 && len(u.Contact) > 0:
return acme.MalformedErr(errors.New("incompatible input; contact and " + return acme.NewError(acme.ErrorMalformedType, "incompatible input; contact and "+
"status updates are mutually exclusive")) "status updates are mutually exclusive")
case len(u.Contact) > 0: case len(u.Contact) > 0:
if err := validateContacts(u.Contact); err != nil { if err := validateContacts(u.Contact); err != nil {
return err return err
@ -60,8 +60,8 @@ func (u *UpdateAccountRequest) Validate() error {
return nil return nil
case len(u.Status) > 0: case len(u.Status) > 0:
if u.Status != string(acme.StatusDeactivated) { if u.Status != string(acme.StatusDeactivated) {
return acme.MalformedErr(errors.Errorf("cannot update account "+ return acme.NewError(acme.ErrorMalformedType, "cannot update account "+
"status to %s, only deactivated", u.Status)) "status to %s, only deactivated", u.Status)
} }
return nil return nil
default: default:
@ -80,8 +80,8 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
} }
var nar NewAccountRequest var nar NewAccountRequest
if err := json.Unmarshal(payload.value, &nar); err != nil { if err := json.Unmarshal(payload.value, &nar); err != nil {
api.WriteError(w, acme.MalformedErr(errors.Wrap(err, api.WriteError(w, acme.ErrorWrap(acme.ErrorMalformedType, err,
"failed to unmarshal new-account request payload"))) "failed to unmarshal new-account request payload"))
return return
} }
if err := nar.Validate(); err != nil { if err := nar.Validate(); err != nil {
@ -101,7 +101,8 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
// Account does not exist // // Account does not exist //
if nar.OnlyReturnExisting { if nar.OnlyReturnExisting {
api.WriteError(w, acme.AccountDoesNotExistErr(nil)) api.WriteError(w, acme.NewError(acme.ErrorAccountDoesNotExistType,
"account does not exist"))
return return
} }
jwk, err := acme.JwkFromContext(r.Context()) jwk, err := acme.JwkFromContext(r.Context())
@ -146,7 +147,8 @@ func (h *Handler) GetUpdateAccount(w http.ResponseWriter, r *http.Request) {
if !payload.isPostAsGet { if !payload.isPostAsGet {
var uar UpdateAccountRequest var uar UpdateAccountRequest
if err := json.Unmarshal(payload.value, &uar); err != nil { 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 return
} }
if err := uar.Validate(); err != nil { if err := uar.Validate(); err != nil {

View file

@ -23,11 +23,11 @@ type NewOrderRequest struct {
// Validate validates a new-order request body. // Validate validates a new-order request body.
func (n *NewOrderRequest) Validate() error { func (n *NewOrderRequest) Validate() error {
if len(n.Identifiers) == 0 { 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 { for _, id := range n.Identifiers {
if id.Type != "dns" { 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 return nil

View file

@ -8,10 +8,12 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/randutil"
) )
// Interface is the acme authority interface. // 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. // NewAccount creates, stores, and returns a new ACME account.
func (a *Authority) NewAccount(ctx context.Context, acc *Account) error { func (a *Authority) NewAccount(ctx context.Context, acc *Account) error {
if err := a.db.CreateAccount(ctx, acc); err != nil { if err := a.db.CreateAccount(ctx, acc); err != nil {
return ErrorWrap(ErrorServerInternalType, err, "error creating account") return ErrorISEWrap(err, "error creating account")
} }
return nil return nil
} }
@ -136,7 +138,7 @@ func (a *Authority) UpdateAccount(ctx context.Context, acc *Account) (*Account,
acc.Status = auo.Status acc.Status = auo.Status
*/ */
if err := a.db.UpdateAccount(ctx, acc); err != nil { 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 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) { func (a *Authority) GetAccount(ctx context.Context, id string) (*Account, error) {
acc, err := a.db.GetAccount(ctx, id) acc, err := a.db.GetAccount(ctx, id)
if err != nil { if err != nil {
return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving account") return nil, ErrorISEWrap(err, "error retrieving account")
} }
return acc, nil 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) o, err := a.db.GetOrder(ctx, orderID)
if err != nil { if err != nil {
return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving order") return nil, ErrorISEWrap(err, "error retrieving order")
} }
if accID != o.AccountID { if accID != o.AccountID {
log.Printf("account-id from request ('%s') does not match order account-id ('%s')", 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") return nil, NewError(ErrorUnauthorizedType, "provisioner does not own order")
} }
if err = o.UpdateStatus(ctx, a.db); err != nil { 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 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. // NewOrder generates, stores, and returns a new ACME order.
func (a *Authority) NewOrder(ctx context.Context, o *Order) (*Order, error) { func (a *Authority) NewOrder(ctx context.Context, o *Order) error {
prov, err := ProvisionerFromContext(ctx) if len(o.AccountID) == 0 {
if err != nil { return NewErrorISE("account-id cannot be empty")
return nil, err }
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 { o.AuthorizationIDs = make([]string, len(o.Identifiers))
return nil, ErrorWrap(ErrorServerInternalType, err, "error creating order") 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. // 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) o, err := a.db.GetOrder(ctx, orderID)
if err != nil { if err != nil {
return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving order") return nil, ErrorISEWrap(err, "error retrieving order")
} }
if accID != o.AccountID { if accID != o.AccountID {
log.Printf("account-id from request ('%s') does not match order account-id ('%s')", 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") return nil, NewError(ErrorUnauthorizedType, "provisioner does not own order")
} }
if err = o.Finalize(ctx, a.db, csr, a.signAuth, prov); err != nil { 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 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. // 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) az, err := a.db.GetAuthorization(ctx, authzID)
if err != nil { if err != nil {
return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving authorization") return nil, ErrorISEWrap(err, "error retrieving authorization")
} }
if accID != az.AccountID { if accID != az.AccountID {
log.Printf("account-id from request ('%s') does not match authz account-id ('%s')", 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") return nil, NewError(ErrorUnauthorizedType, "account does not own order")
} }
if err = az.UpdateStatus(ctx, a.db); err != nil { 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 return az, nil
} }
// ValidateChallenge attempts to validate the challenge. // NewChallenge generates and stores an ACME challenge and associated resources.
func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, jwk *jose.JSONWebKey) (*Challenge, error) { 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") ch, err := a.db.GetChallenge(ctx, chID, "todo")
if err != nil { if err != nil {
return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving challenge") return nil, ErrorISEWrap(err, "error retrieving challenge")
} }
if accID != ch.AccountID { if accID != ch.AccountID {
log.Printf("account-id from request ('%s') does not match challenge account-id ('%s')", 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) return tls.DialWithDialer(dialer, network, addr, config)
}, },
}); err != nil { }); err != nil {
return nil, ErrorWrap(ErrorServerInternalType, err, "error validating challenge") return nil, ErrorISEWrap(err, "error validating challenge")
} }
return ch, nil 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) { func (a *Authority) GetCertificate(ctx context.Context, accID, certID string) ([]byte, error) {
cert, err := a.db.GetCertificate(ctx, certID) cert, err := a.db.GetCertificate(ctx, certID)
if err != nil { if err != nil {
return nil, ErrorWrap(ErrorServerInternalType, err, "error retrieving certificate") return nil, ErrorISEWrap(err, "error retrieving certificate")
} }
if cert.AccountID != accID { if cert.AccountID != accID {
log.Printf("account-id from request ('%s') does not match challenge account-id ('%s')", accID, cert.AccountID) log.Printf("account-id from request ('%s') does not match challenge account-id ('%s')", accID, cert.AccountID)

View file

@ -8,20 +8,22 @@ import (
// Authorization representst an ACME Authorization. // Authorization representst an ACME Authorization.
type Authorization struct { type Authorization struct {
Identifier *Identifier `json:"identifier"` Identifier Identifier `json:"identifier"`
Status Status `json:"status"` Status Status `json:"status"`
Expires string `json:"expires"` Expires string `json:"expires"`
Challenges []*Challenge `json:"challenges"` Challenges []*Challenge `json:"challenges"`
Wildcard bool `json:"wildcard"` ChallengeIDs string `json::"-"`
ID string `json:"-"` Wildcard bool `json:"wildcard"`
AccountID string `json:"-"` ID string `json:"-"`
AccountID string `json:"-"`
Token string `json:"-"`
} }
// ToLog enables response logging. // ToLog enables response logging.
func (az *Authorization) ToLog() (interface{}, error) { func (az *Authorization) ToLog() (interface{}, error) {
b, err := json.Marshal(az) b, err := json.Marshal(az)
if err != nil { 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 return string(b), nil
} }
@ -32,7 +34,7 @@ func (az *Authorization) UpdateStatus(ctx context.Context, db DB) error {
now := time.Now().UTC() now := time.Now().UTC()
expiry, err := time.Parse(time.RFC3339, az.Expires) expiry, err := time.Parse(time.RFC3339, az.Expires)
if err != nil { if err != nil {
return ErrorInternalServerWrap(err, "error converting expiry string to time") return ErrorISEWrap(err, "error converting expiry string to time")
} }
switch az.Status { 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 { if err = db.UpdateAuthorization(ctx, az); err != nil {
return ErrorInternalServerWrap(err, "error updating authorization") return ErrorISEWrap(err, "error updating authorization")
} }
return nil return nil
} }

View file

@ -38,7 +38,7 @@ type Challenge struct {
func (ch *Challenge) ToLog() (interface{}, error) { func (ch *Challenge) ToLog() (interface{}, error) {
b, err := json.Marshal(ch) b, err := json.Marshal(ch)
if err != nil { 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 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) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return ErrorInternalServerWrap(err, "error reading "+ return ErrorISEWrap(err, "error reading "+
"response body for url %s", url) "response body for url %s", url)
} }
keyAuth := strings.Trim(string(body), "\r\n") 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) ch.Validated = clock.Now().Format(time.RFC3339)
if err = db.UpdateChallenge(ctx, ch); err != nil { if err = db.UpdateChallenge(ctx, ch); err != nil {
return ErrorInternalServerWrap(err, "error updating challenge") return ErrorISEWrap(err, "error updating challenge")
} }
return nil 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) ch.Validated = clock.Now().Format(time.RFC3339)
if err = db.UpdateChallenge(ctx, ch); err != nil { if err = db.UpdateChallenge(ctx, ch); err != nil {
return ErrorInternalServerWrap(err, "tlsalpn01ValidateChallenge - error updating challenge") return ErrorISEWrap(err, "tlsalpn01ValidateChallenge - error updating challenge")
} }
return nil 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) ch.Validated = clock.Now().UTC().Format(time.RFC3339)
if err = db.UpdateChallenge(ctx, ch); err != nil { if err = db.UpdateChallenge(ctx, ch); err != nil {
return ErrorInternalServerWrap(err, "error updating challenge") return ErrorISEWrap(err, "error updating challenge")
} }
return nil 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) { func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) {
thumbprint, err := jwk.Thumbprint(crypto.SHA256) thumbprint, err := jwk.Thumbprint(crypto.SHA256)
if err != nil { if err != nil {
return "", ErrorInternalServerWrap(err, "error generating JWK thumbprint") return "", ErrorISEWrap(err, "error generating JWK thumbprint")
} }
encPrint := base64.RawURLEncoding.EncodeToString(thumbprint) encPrint := base64.RawURLEncoding.EncodeToString(thumbprint)
return fmt.Sprintf("%s.%s", token, encPrint), nil 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 { func storeError(ctx context.Context, ch *Challenge, db DB, err *Error) error {
ch.Error = err ch.Error = err
if err := db.UpdateChallenge(ctx, ch); err != nil { 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 return nil
} }

View file

@ -3,7 +3,6 @@ package nosql
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"strings"
"time" "time"
"github.com/pkg/errors" "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. // CreateAuthorization creates an entry in the database for the Authorization.
// Implements the acme.DB.CreateAuthorization interface. // Implements the acme.DB.CreateAuthorization interface.
func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) error { 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 var err error
az.ID, err = randID() az.ID, err = randID()
if err != nil { if err != nil {
return err return err
} }
chIDs := make([]string, len(az.Challenges))
for i, ch := range az.Challenges {
chIDs[i] = ch.ID
}
now := clock.Now() now := clock.Now()
dbaz := &dbAuthz{ dbaz := &dbAuthz{
ID: az.ID, ID: az.ID,
@ -95,38 +93,10 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) e
Created: now, Created: now,
Expires: now.Add(defaultExpiryDuration), Expires: now.Add(defaultExpiryDuration),
Identifier: az.Identifier, 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) return db.save(ctx, az.ID, dbaz, nil, "authz", authzTable)
} }

View file

@ -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. // CreateChallenge creates a new ACME challenge data structure in the database.
// Implements acme.DB.CreateChallenge interface. // Implements acme.DB.CreateChallenge interface.
func (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error { 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 var err error
ch.ID, err = randID() ch.ID, err = randID()
if err != nil { if err != nil {
return errors.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 errors.Wrap(err, "error generating token for ACME challenge")
}
dbch := &dbChallenge{ dbch := &dbChallenge{
ID: ch.ID, ID: ch.ID,

View file

@ -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. // CreateOrder creates ACME Order resources and saves them to the DB.
func (db *DB) CreateOrder(ctx context.Context, o *acme.Order) error { 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() o.ID, err = randID()
if err != nil { if err != nil {
return nil, err 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() 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{ dbo := &dbOrder{
ID: o.ID, ID: o.ID,
AccountID: o.AccountID, AccountID: o.AccountID,
@ -123,10 +87,10 @@ func (db *DB) CreateOrder(ctx context.Context, o *acme.Order) error {
Created: now, Created: now,
Status: StatusPending, Status: StatusPending,
Expires: now.Add(defaultOrderExpiry), Expires: now.Add(defaultOrderExpiry),
Identifiers: ops.Identifiers, Identifiers: o.Identifiers,
NotBefore: nbf.Add(backdate), NotBefore: o.NotBefore,
NotAfter: naf, NotAfter: o.NotBefore,
Authorizations: azIDs, Authorizations: o.AuthorizationIDs,
} }
if err := db.save(ctx, o.ID, dbo, nil, orderTable); err != nil { if err := db.save(ctx, o.ID, dbo, nil, orderTable); err != nil {
return nil, err return nil, err

View file

@ -21,7 +21,7 @@ type Directory struct {
func (d *Directory) ToLog() (interface{}, error) { func (d *Directory) ToLog() (interface{}, error) {
b, err := json.Marshal(d) b, err := json.Marshal(d)
if err != nil { 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 return string(b), nil
} }

View file

@ -1,4 +1,3 @@
// Error represents an ACME
package acme package acme
import ( import (
@ -11,55 +10,55 @@ import (
type ProblemType int type ProblemType int
const ( const (
// The request specified an account that does not exist // ErrorAccountDoesNotExistType request specified an account that does not exist
ErrorAccountDoesNotExistType ProblemType = iota 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 ErrorAlreadyRevokedType
// The CSR is unacceptable (e.g., due to a short key) // ErrorBadCSRType CSR is unacceptable (e.g., due to a short key)
ErrorBadCSRType ErrorBadCSRType
// The client sent an unacceptable anti-replay nonce // ErrorBadNonceType client sent an unacceptable anti-replay nonce
ErrorBadNonceType 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 ErrorBadPublicKeyType
// The revocation reason provided is not allowed by the server // ErrorBadRevocationReasonType revocation reason provided is not allowed by the server
ErrorBadRevocationReasonType 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 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 ErrorCaaType
// Specific error conditions are indicated in the “subproblems” array. // ErrorCompoundType error conditions are indicated in the “subproblems” array.
ErrorCompoundType ErrorCompoundType
// The server could not connect to validation target // ErrorConnectionType server could not connect to validation target
ErrorConnectionType ErrorConnectionType
// There was a problem with a DNS query during identifier validation // ErrorDNSType was a problem with a DNS query during identifier validation
ErrorDNSType ErrorDNSType
// The request must include a value for the “externalAccountBinding” field // ErrorExternalAccountRequiredType request must include a value for the “externalAccountBinding” field
ErrorExternalAccountRequiredType ErrorExternalAccountRequiredType
// Response received didnt match the challenges requirements // ErrorIncorrectResponseType received didnt match the challenges requirements
ErrorIncorrectResponseType ErrorIncorrectResponseType
// A contact URL for an account was invalid // ErrorInvalidContactType URL for an account was invalid
ErrorInvalidContactType ErrorInvalidContactType
// The request message was malformed // ErrorMalformedType request message was malformed
ErrorMalformedType 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 ErrorOrderNotReadyType
// The request exceeds a rate limit // ErrorRateLimitedType request exceeds a rate limit
ErrorRateLimitedType ErrorRateLimitedType
// The server will not issue certificates for the identifier // ErrorRejectedIdentifierType server will not issue certificates for the identifier
ErrorRejectedIdentifierType ErrorRejectedIdentifierType
// The server experienced an internal error // ErrorServerInternalType server experienced an internal error
ErrorServerInternalType ErrorServerInternalType
// The server received a TLS error during validation // ErrorTLSType server received a TLS error during validation
ErrorTLSType ErrorTLSType
// The client lacks sufficient authorization // ErrorUnauthorizedType client lacks sufficient authorization
ErrorUnauthorizedType ErrorUnauthorizedType
// A contact URL for an account used an unsupported protocol scheme // ErrorUnsupportedContactType URL for an account used an unsupported protocol scheme
ErrorUnsupportedContactType ErrorUnsupportedContactType
// An identifier is of an unsupported type // ErrorUnsupportedIdentifierType identifier is of an unsupported type
ErrorUnsupportedIdentifierType ErrorUnsupportedIdentifierType
// Visit the “instance” URL and take actions specified there // ErrorUserActionRequiredType the “instance” URL and take actions specified there
ErrorUserActionRequiredType ErrorUserActionRequiredType
// The operation is not implemented // ErrorNotImplementedType operation is not implemented
ErrorNotImplementedType ErrorNotImplementedType
) )
@ -116,7 +115,7 @@ func (ap ProblemType) String() string {
case ErrorNotImplementedType: case ErrorNotImplementedType:
return "notImplemented" return "notImplemented"
default: 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:"-"` Status int `json:"-"`
} }
// NewError creates a new Error type.
func NewError(pt ProblemType, msg string, args ...interface{}) *Error { func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
meta, ok := errorMap[pt] meta, ok := errorMap[pt]
if !ok { 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. // ErrorWrap attempts to wrap the internal error.
func ErrorWrap(typ ProblemType, err error, msg string, args ...interface{}) *Error { func ErrorWrap(typ ProblemType, err error, msg string, args ...interface{}) *Error {
switch e := err.(type) { 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. // ErrorISEWrap shortcut to wrap an internal server error type.
func ErrorInternalServerWrap(err error, msg string, args ...interface{}) *Error { func ErrorISEWrap(err error, msg string, args ...interface{}) *Error {
return ErrorWrap(ErrorServerInternalType, err, msg, args...) return ErrorWrap(ErrorServerInternalType, err, msg, args...)
} }

View file

@ -20,27 +20,28 @@ type Identifier struct {
// Order contains order metadata for the ACME protocol order type. // Order contains order metadata for the ACME protocol order type.
type Order struct { type Order struct {
Status Status `json:"status"` Status Status `json:"status"`
Expires string `json:"expires,omitempty"` Expires time.Time `json:"expires,omitempty"`
Identifiers []Identifier `json:"identifiers"` Identifiers []Identifier `json:"identifiers"`
NotBefore string `json:"notBefore,omitempty"` NotBefore time.Time `json:"notBefore,omitempty"`
NotAfter string `json:"notAfter,omitempty"` NotAfter time.Time `json:"notAfter,omitempty"`
Error interface{} `json:"error,omitempty"` Error interface{} `json:"error,omitempty"`
Authorizations []string `json:"authorizations"` AuthorizationURLs []string `json:"authorizations"`
FinalizeURL string `json:"finalize"` AuthorizationIDs []string `json:"-"`
Certificate string `json:"certificate,omitempty"` FinalizeURL string `json:"finalize"`
ID string `json:"-"` Certificate string `json:"certificate,omitempty"`
AccountID string `json:"-"` ID string `json:"-"`
ProvisionerID string `json:"-"` AccountID string `json:"-"`
DefaultDuration time.Duration `json:"-"` ProvisionerID string `json:"-"`
Backdate time.Duration `json:"-"` DefaultDuration time.Duration `json:"-"`
Backdate time.Duration `json:"-"`
} }
// ToLog enables response logging. // ToLog enables response logging.
func (o *Order) ToLog() (interface{}, error) { func (o *Order) ToLog() (interface{}, error) {
b, err := json.Marshal(o) b, err := json.Marshal(o)
if err != nil { 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 return string(b), nil
} }
@ -49,10 +50,6 @@ func (o *Order) ToLog() (interface{}, error) {
// Changes to the order are saved using the database interface. // Changes to the order are saved using the database interface.
func (o *Order) UpdateStatus(ctx context.Context, db DB) error { func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
now := time.Now().UTC() 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 { switch o.Status {
case StatusInvalid: case StatusInvalid:
@ -61,7 +58,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
return nil return nil
case StatusReady: case StatusReady:
// Check expiry // Check expiry
if now.After(expiry) { if now.After(o.Expires) {
o.Status = StatusInvalid o.Status = StatusInvalid
o.Error = NewError(ErrorMalformedType, "order has expired") o.Error = NewError(ErrorMalformedType, "order has expired")
break break
@ -69,7 +66,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
return nil return nil
case StatusPending: case StatusPending:
// Check expiry // Check expiry
if now.After(expiry) { if now.After(o.Expires) {
o.Status = StatusInvalid o.Status = StatusInvalid
o.Error = NewError(ErrorMalformedType, "order has expired") o.Error = NewError(ErrorMalformedType, "order has expired")
break break
@ -80,7 +77,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
StatusInvalid: 0, StatusInvalid: 0,
StatusPending: 0, StatusPending: 0,
} }
for _, azID := range o.Authorizations { for _, azID := range o.AuthorizationIDs {
az, err := db.GetAuthorization(ctx, azID) az, err := db.GetAuthorization(ctx, azID)
if err != nil { if err != nil {
return err return err
@ -100,14 +97,14 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
case count[StatusPending] > 0: case count[StatusPending] > 0:
return nil return nil
case count[StatusValid] == len(o.Authorizations): case count[StatusValid] == len(o.AuthorizationIDs):
o.Status = StatusReady o.Status = StatusReady
default: default:
return NewError(ErrorServerInternalType, "unexpected authz status") return NewErrorISE("unexpected authz status")
} }
default: default:
return NewError(ErrorServerInternalType, "unrecognized order status: %s", o.Status) return NewErrorISE("unrecognized order status: %s", o.Status)
} }
return db.UpdateOrder(ctx, o) return db.UpdateOrder(ctx, o)
} }
@ -129,7 +126,7 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
case StatusReady: case StatusReady:
break break
default: 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 // 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) ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
signOps, err := p.AuthorizeSign(ctx, "") signOps, err := p.AuthorizeSign(ctx, "")
if err != nil { 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 // 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) templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data)
if err != nil { 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) 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. // Sign a new certificate.
certChain, err := auth.Sign(csr, provisioner.SignOptions{ certChain, err := auth.Sign(csr, provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(nbf), NotBefore: provisioner.NewTimeDuration(o.NotBefore),
NotAfter: provisioner.NewTimeDuration(naf), NotAfter: provisioner.NewTimeDuration(o.NotAfter),
}, signOps...) }, signOps...)
if err != nil { 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{ cert := &Certificate{

View file

@ -14,10 +14,9 @@ import (
// WriteError writes to w a JSON representation of the given error. // WriteError writes to w a JSON representation of the given error.
func WriteError(w http.ResponseWriter, err error) { func WriteError(w http.ResponseWriter, err error) {
switch k := err.(type) { switch err.(type) {
case *acme.Error: case *acme.Error:
w.Header().Set("Content-Type", "application/problem+json") w.Header().Set("Content-Type", "application/problem+json")
err = k.ToACME()
default: default:
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
} }