From 461bad3fefce23d0668284296e414ee206fdf863 Mon Sep 17 00:00:00 2001 From: max furman Date: Sat, 27 Feb 2021 17:05:37 -0800 Subject: [PATCH] [acme db interface] wip --- acme/{types => }/account.go | 0 acme/authority.go | 83 ++++---- acme/authorization.go | 75 +++++++ acme/challenge.go | 262 +++++++++++++++++++++++ acme/common.go | 11 + acme/{db => }/db.go | 0 acme/db/nosql/account.go | 52 +---- acme/db/nosql/authz.go | 110 ++-------- acme/db/nosql/challenge.go | 403 +----------------------------------- acme/db/nosql/nonce.go | 12 +- acme/db/nosql/nosql.go | 46 ++++ acme/db/nosql/order.go | 298 ++++---------------------- acme/errors.go | 3 + acme/{types => }/nonce.go | 0 acme/order.go | 371 +++++---------------------------- acme/{types => }/status.go | 0 acme/types/authz.go | 27 --- acme/types/challenge.go | 30 --- acme/types/order.go | 31 --- 19 files changed, 565 insertions(+), 1249 deletions(-) rename acme/{types => }/account.go (100%) create mode 100644 acme/authorization.go create mode 100644 acme/challenge.go rename acme/{db => }/db.go (100%) rename acme/{types => }/nonce.go (100%) rename acme/{types => }/status.go (100%) delete mode 100644 acme/types/authz.go delete mode 100644 acme/types/challenge.go delete mode 100644 acme/types/order.go diff --git a/acme/types/account.go b/acme/account.go similarity index 100% rename from acme/types/account.go rename to acme/account.go diff --git a/acme/authority.go b/acme/authority.go index c9190811..c0b6a732 100644 --- a/acme/authority.go +++ b/acme/authority.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "log" "net" "net/http" "net/url" @@ -69,17 +70,6 @@ type AuthorityOptions struct { Prefix string } -var ( - accountTable = []byte("acme_accounts") - accountByKeyIDTable = []byte("acme_keyID_accountID_index") - authzTable = []byte("acme_authzs") - challengeTable = []byte("acme_challenges") - nonceTable = []byte("nonces") - orderTable = []byte("acme_orders") - ordersByAccountIDTable = []byte("acme_account_orders_index") - certTable = []byte("acme_certs") -) - // NewAuthority returns a new Authority that implements the ACME interface. // // Deprecated: NewAuthority exists for hitorical compatibility and should not @@ -197,14 +187,23 @@ func (a *Authority) GetAccountByKey(ctx context.Context, jwk *jose.JSONWebKey) ( // GetOrder returns an ACME order. func (a *Authority) GetOrder(ctx context.Context, accID, orderID string) (*Order, error) { - o, err := getOrder(a.db, orderID) + prov, err := ProvisionerFromContext(ctx) if err != nil { return nil, err } + o, err := a.db.GetOrder(ctx, orderID) + if err != nil { + return nil, ServerInternalErr(err) + } if accID != o.AccountID { + log.Printf("account-id from request ('%s') does not match order account-id ('%s')", accID, o.AccountID) return nil, UnauthorizedErr(errors.New("account does not own order")) } - if o, err = o.updateStatus(a.db); err != nil { + if prov.GetID() != o.ProvisionerID { + 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 { return nil, err } return o.toACME(ctx, a.db, a.dir) @@ -234,13 +233,15 @@ func (a *Authority) NewOrder(ctx context.Context, ops OrderOptions) (*Order, err if err != nil { return nil, err } - ops.backdate = a.backdate.Duration - ops.defaultDuration = prov.DefaultTLSCertDuration() - order, err := newOrder(a.db, ops) - if err != nil { - return nil, Wrap(err, "error creating order") - } - return order.toACME(ctx, a.db, a.dir) + return db.CreateOrder(ctx, &Order{ + AccountID: ops.AccountID, + ProvisionerID: prov.GetID(), + Backdate: a.backdate.Duration, + DefaultDuration: prov.DefaultTLSCertDuration(), + Identifiers: ops.Identifiers, + NotBefore: ops.NotBefore, + NotAfter: ops.NotAfter, + }) } // FinalizeOrder attempts to finalize an order and generate a new certificate. @@ -249,44 +250,51 @@ func (a *Authority) FinalizeOrder(ctx context.Context, accID, orderID string, cs if err != nil { return nil, err } - o, err := getOrder(a.db, orderID) + o, err := a.db.GetOrder(ctx, orderID) if err != nil { return nil, err } if accID != o.AccountID { + log.Printf("account-id from request ('%s') does not match order account-id ('%s')", accID, o.AccountID) return nil, UnauthorizedErr(errors.New("account does not own order")) } - o, err = o.finalize(a.db, csr, a.signAuth, prov) + if prov.GetID() != o.ProvisionerID { + 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 { return nil, Wrap(err, "error finalizing order") } - return o.toACME(ctx, a.db, a.dir) + return o, nil } // GetAuthz retrieves and attempts to update the status on an ACME authz // before returning. func (a *Authority) GetAuthz(ctx context.Context, accID, authzID string) (*Authz, error) { - az, err := getAuthz(a.db, authzID) + az, err := a.db.GetAuthorization(ctx, authzID) if err != nil { return nil, err } - if accID != az.getAccountID() { + if accID != az.AccountID { + 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(a.db) + az, err = az.UpdateStatus(ctx, a.db) if err != nil { return nil, Wrap(err, "error updating authz status") } - return az.toACME(ctx, a.db, a.dir) + return az, nil } // ValidateChallenge attempts to validate the challenge. func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, jwk *jose.JSONWebKey) (*Challenge, error) { - ch, err := getChallenge(a.db, chID) + ch, err := a.db.GetChallenge(ctx, chID, "todo") if err != nil { return nil, err } - if accID != ch.getAccountID() { + if accID != ch.AccountID { + log.Printf("account-id from request ('%s') does not match challenge account-id ('%s')", accID, ch.AccountID) return nil, UnauthorizedErr(errors.New("account does not own challenge")) } client := http.Client{ @@ -295,17 +303,16 @@ func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, j dialer := &net.Dialer{ Timeout: 30 * time.Second, } - ch, err = ch.validate(a.db, jwk, validateOptions{ + if err = ch.Validate(ctx, a.db, jwk, validateOptions{ httpGet: client.Get, lookupTxt: net.LookupTXT, tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { return tls.DialWithDialer(dialer, network, addr, config) }, - }) - if err != nil { + }); err != nil { return nil, Wrap(err, "error attempting challenge validation") } - return ch.toACME(ctx, a.db, a.dir) + return ch, nil } // GetCertificate retrieves the Certificate by ID. @@ -319,13 +326,3 @@ func (a *Authority) GetCertificate(accID, certID string) ([]byte, error) { } return cert.toACME(a.db, a.dir) } - -type httpGetter func(string) (*http.Response, error) -type lookupTxt func(string) ([]string, error) -type tlsDialer func(network, addr string, config *tls.Config) (*tls.Conn, error) - -type validateOptions struct { - httpGet httpGetter - lookupTxt lookupTxt - tlsDial tlsDialer -} diff --git a/acme/authorization.go b/acme/authorization.go new file mode 100644 index 00000000..a41950cd --- /dev/null +++ b/acme/authorization.go @@ -0,0 +1,75 @@ +package types + +import ( + "context" + "encoding/json" + "time" + + "github.com/pkg/errors" +) + +// Authorization representst an ACME Authorization. +type Authorization struct { + Identifier *Identifier `json:"identifier"` + Status string `json:"status"` + Expires string `json:"expires"` + Challenges []*Challenge `json:"challenges"` + Wildcard bool `json:"wildcard"` + ID string `json:"-"` + AccountID string `json:"-"` +} + +// ToLog enables response logging. +func (az *Authorization) ToLog() (interface{}, error) { + b, err := json.Marshal(az) + if err != nil { + return nil, ServerInternalErr(errors.Wrap(err, "error marshaling authz for logging")) + } + return string(b), nil +} + +// UpdateStatus updates the ACME Authorization Status if necessary. +// Changes to the Authorization are saved using the database interface. +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")) + } + + switch az.Status { + case StatusInvalid: + return nil + case StatusValid: + return nil + case StatusPending: + // 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) + } + if ch.Status == StatusValid { + isValid = true + break + } + } + + if !isValid { + return nil + } + az.Status = StatusValid + az.Error = nil + default: + return nil, ServerInternalErr(errors.Errorf("unrecognized authz status: %s", ba.Status)) + } + + return ServerInternalErr(db.UpdateAuthorization(ctx, az)) +} diff --git a/acme/challenge.go b/acme/challenge.go new file mode 100644 index 00000000..de178d6c --- /dev/null +++ b/acme/challenge.go @@ -0,0 +1,262 @@ +package types + +import ( + "context" + "crypto" + "crypto/sha256" + "crypto/subtle" + "crypto/tls" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "strings" + "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"` + Token string `json:"token"` + Validated string `json:"validated,omitempty"` + URL string `json:"url"` + Error *AError `json:"error,omitempty"` + ID string `json:"-"` + AuthzID string `json:"-"` + AccountID string `json:"-"` + Value string `json:"-"` +} + +// ToLog enables response logging. +func (ch *Challenge) ToLog() (interface{}, error) { + b, err := json.Marshal(ch) + if err != nil { + return nil, ServerInternalErr(errors.Wrap(err, "error marshaling challenge for logging")) + } + return string(b), nil +} + +// Validate attempts to validate the challenge. Stores changes to the Challenge +// type using the DB interface. +// satisfactorily validated, the 'status' and 'validated' attributes are +// updated. +func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, vo validateOptions) error { + // If already valid or invalid then return without performing validation. + if ch.Status == StatusValid || ch.Status == StatusInvalid { + return nil + } + switch ch.Type { + case "http-01": + return http01Validate(ctx, ch, db, jwk, vo) + case "dns-01": + return dns01Validate(ctx, ch, db, jwk, vo) + case "tls-alpn-01": + return tlsalpn01Validate(ctx, ch, db, jwk, vo) + default: + return ServerInternalErr(errors.Errorf("unexpected challenge type '%s'", ch.Type)) + } +} + +func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo validateOptions) error { + url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", ch.Value, ch.Token) + + resp, err := vo.httpGet(url) + if err != nil { + return storeError(ctx, ch, db, ConnectionErr(errors.Wrapf(err, + "error doing http GET for url %s", url))) + } + if resp.StatusCode >= 400 { + return storeError(ctx, ch, db, ConnectionErr(errors.Errorf("error doing http GET for url %s with status code %d", + url, resp.StatusCode))) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return ServerInternalErr(errors.Wrapf(err, "error reading "+ + "response body for url %s", url)) + } + keyAuth := strings.Trim(string(body), "\r\n") + + expected, err := KeyAuthorization(ch.Token, jwk) + if err != nil { + return err + } + if keyAuth != expected { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("keyAuthorization does not match; "+ + "expected %s, but got %s", expected, keyAuth))) + } + + // Update and store the challenge. + ch.Status = StatusValid + ch.Error = nil + ch.Validated = clock.Now() + + return ServerInternalErr(db.UpdateChallenge(ctx, ch)) +} + +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, + InsecureSkipVerify: true, // we expect a self-signed challenge certificate + } + + hostPort := net.JoinHostPort(tc.Value, "443") + + conn, err := vo.tlsDial("tcp", hostPort, config) + if err != nil { + return storeError(ctx, ch, db, ConnectionErr(errors.Wrapf(err, + "error doing TLS dial for %s", hostPort))) + } + defer conn.Close() + + cs := conn.ConnectionState() + certs := cs.PeerCertificates + + if len(certs) == 0 { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("%s "+ + "challenge for %s resulted in no certificates", tc.Type, tc.Value))) + } + + if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != "acme-tls/1" { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("cannot "+ + "negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge"))) + } + + leafCert := certs[0] + + if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], tc.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))) + } + + 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) + if err != nil { + return nil, err + } + hashedKeyAuth := sha256.Sum256([]byte(keyAuth)) + + for _, ext := range leafCert.Extensions { + if idPeAcmeIdentifier.Equal(ext.Id) { + if !ext.Critical { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("incorrect "+ + "certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical"))) + } + + var extValue []byte + rest, err := asn1.Unmarshal(ext.Value, &extValue) + + if err != nil || len(rest) > 0 || len(hashedKeyAuth) != len(extValue) { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("incorrect "+ + "certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value"))) + } + + if subtle.ConstantTimeCompare(hashedKeyAuth[:], extValue) != 1 { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ + "expected acmeValidationV1 extension value %s for this challenge but got %s", + hex.EncodeToString(hashedKeyAuth[:]), hex.EncodeToString(extValue)))) + } + + ch.Status = StatusValid + ch.Error = nil + ch.Validated = clock.Now() + + return ServerInternalErr(db.UpdateChallenge(ctx, ch)) + } + + if idPeAcmeIdentifierV1Obsolete.Equal(ext.Id) { + foundIDPeAcmeIdentifierV1Obsolete = true + } + } + + if foundIDPeAcmeIdentifierV1Obsolete { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("incorrect "+ + "certificate for tls-alpn-01 challenge: obsolete id-pe-acmeIdentifier in acmeValidationV1 extension"))) + } + + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("incorrect "+ + "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 { + // 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, "*.") + + txtRecords, err := vo.lookupTxt("_acme-challenge." + domain) + if err != nil { + return storeError(ctx, ch, db, DNSErr(errors.Wrapf(err, "error looking up TXT "+ + "records for domain %s", domain))) + } + + expectedKeyAuth, err := KeyAuthorization(dc.Token, jwk) + if err != nil { + return nil, err + } + h := sha256.Sum256([]byte(expectedKeyAuth)) + expected := base64.RawURLEncoding.EncodeToString(h[:]) + var found bool + for _, r := range txtRecords { + if r == expected { + found = true + break + } + } + if !found { + return storeError(ctx, ch, db, RejectedIdentifierErr(errors.Errorf("keyAuthorization "+ + "does not match; expected %s, but got %s", expectedKeyAuth, txtRecords))) + } + + // Update and store the challenge. + ch.Status = StatusValid + ch.Error = nil + ch.Validated = time.Now().UTC() + + return ServerInternalErr(db.UpdateChallenge(ctx, ch)) +} + +// KeyAuthorization creates the ACME key authorization value from a token +// and a jwk. +func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) { + thumbprint, err := jwk.Thumbprint(crypto.SHA256) + if err != nil { + return "", ServerInternalErr(errors.Wrap(err, "error generating JWK thumbprint")) + } + encPrint := base64.RawURLEncoding.EncodeToString(thumbprint) + return fmt.Sprintf("%s.%s", token, encPrint), nil +} + +// 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 { + ch.Error = err.ToACME() + if err := db.UpdateChallenge(ctx, ch); err != nil { + return ServerInternalErr(errors.Wrap(err, "failure saving error to acme challenge")) + } + return nil +} + +type httpGetter func(string) (*http.Response, error) +type lookupTxt func(string) ([]string, error) +type tlsDialer func(network, addr string, config *tls.Config) (*tls.Conn, error) + +type validateOptions struct { + httpGet httpGetter + lookupTxt lookupTxt + tlsDial tlsDialer +} diff --git a/acme/common.go b/acme/common.go index fec47b94..a5a1fe09 100644 --- a/acme/common.go +++ b/acme/common.go @@ -16,6 +16,7 @@ import ( // only those methods required by the ACME api/authority. type Provisioner interface { AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) + GetID() string GetName() string DefaultTLSCertDuration() time.Duration GetOptions() *provisioner.Options @@ -25,6 +26,7 @@ type Provisioner interface { type MockProvisioner struct { Mret1 interface{} Merr error + MgetID func() string MgetName func() string MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error) MdefaultTLSCertDuration func() time.Duration @@ -55,6 +57,7 @@ func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration { return m.Mret1.(time.Duration) } +// GetOptions mock func (m *MockProvisioner) GetOptions() *provisioner.Options { if m.MgetOptions != nil { return m.MgetOptions() @@ -62,6 +65,14 @@ func (m *MockProvisioner) GetOptions() *provisioner.Options { return m.Mret1.(*provisioner.Options) } +// GetID mock +func (m *MockProvisioner) GetID() string { + if m.MgetID != nil { + return m.MgetID() + } + return m.Mret1.(string) +} + // ContextKey is the key type for storing and searching for ACME request // essentials in the context of a request. type ContextKey string diff --git a/acme/db/db.go b/acme/db.go similarity index 100% rename from acme/db/db.go rename to acme/db.go diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index dd93c5d2..6e9ee8c0 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -25,45 +25,15 @@ func (dba *dbAccount) clone() *dbAccount { return &nu } -func (db *DB) saveDBAccount(nu *dbAccount, old *dbAccount) error { - var ( - err error - oldB []byte - ) - if old == nil { - oldB = nil - } else { - if oldB, err = json.Marshal(old); err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling old acme order")) - } - } - - b, err := json.Marshal(*nu) - if err != nil { - return errors.Wrap(err, "error marshaling new account object") - } - // Set the Account - _, swapped, err := db.CmpAndSwap(accountTable, []byte(nu.ID), oldB, b) - switch { - case err != nil: - return ServerInternalErr(errors.Wrap(err, "error storing account")) - case !swapped: - return ServerInternalErr(errors.New("error storing account; " + - "value has changed since last read")) - default: - return nil - } -} - // CreateAccount imlements the AcmeDB.CreateAccount interface. func (db *DB) CreateAccount(ctx context.Context, acc *types.Account) error { - id, err := randID() + acc.ID, err = randID() if err != nil { return nil, err } dba := &dbAccount{ - ID: id, + ID: acc.ID, Key: acc.Key, Contact: acc.Contact, Status: acc.Valid, @@ -84,7 +54,7 @@ func (db *DB) CreateAccount(ctx context.Context, acc *types.Account) error { case !swapped: return ServerInternalErr(errors.Errorf("key-id to account-id index already exists")) default: - if err = db.saveDBAccount(dba, nil); err != nil { + if err = db.save(ctx, acc.ID, dba, nil, "account", accountTable); err != nil { db.db.Del(accountByKeyIDTable, kidB) return err } @@ -115,9 +85,11 @@ func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*types.Account // UpdateAccount imlements the AcmeDB.UpdateAccount interface. func (db *DB) UpdateAccount(ctx context.Context, acc *types.Account) error { - kid := "from-context" + if len(acc.ID) == 0 { + return ServerInternalErr(errors.New("id cannot be empty")) + } - old, err := db.getDBAccountByKeyID(ctx, kid) + old, err := db.getDBAccount(ctx, acc.ID) if err != nil { return err } @@ -131,7 +103,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *types.Account) error { nu.Deactivated = clock.Now() } - return db.saveDBAccount(newdba, dba) + return db.save(ctx, old.ID, newdba, dba, "account", accountTable) } func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) { @@ -145,12 +117,8 @@ func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, erro return string(id), nil } -// getDBAccountByKeyID retrieves Id associated with the given Kid. -func (db *DB) getDBAccountByKeyID(ctx context.Context, kid string) (*dbAccount, error) { - id, err := db.getAccountIDByKeyID(ctx, kid) - if err != nil { - return err - } +// getDBAccount retrieves and unmarshals dbAccount. +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) { diff --git a/acme/db/nosql/authz.go b/acme/db/nosql/authz.go index 32e42a69..a50d46f1 100644 --- a/acme/db/nosql/authz.go +++ b/acme/db/nosql/authz.go @@ -14,15 +14,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 *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"` } func (ba *dbAuthz) clone() *dbAuthz { @@ -30,34 +30,6 @@ func (ba *dbAuthz) clone() *dbAuthz { return &u } -func (db *DB) saveDBAuthz(ctx context.Context, nu *authz, old *dbAuthz) error { - var ( - err error - oldB, newB []byte - ) - - if old == nil { - oldB = nil - } else { - if oldB, err = json.Marshal(old); err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling old authz")) - } - } - if newB, err = json.Marshal(nu); err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling new authz")) - } - _, swapped, err := db.CmpAndSwap(authzTable, []byte(ba.ID), oldB, newB) - switch { - case err != nil: - return ServerInternalErr(errors.Wrapf(err, "error storing authz")) - case !swapped: - return ServerInternalErr(errors.Errorf("error storing authz; " + - "value has changed since last read")) - default: - return nil - } -} - // getDBAuthz retrieves and unmarshals a database representation of the // ACME Authorization type. func (db *DB) getDBAuthz(ctx context.Context, id string) (*dbAuthz, error) { @@ -102,8 +74,11 @@ 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 { - if len(authz.AccountID) == 0 { - return ServerInternalErr(errors.New("AccountID cannot be empty")) + if len(az.AccountID) == 0 { + return ServerInternalErr(errors.New("account-id cannot be empty")) + } + if az.Identifier == nil { + return ServerInternalErr(errors.New("identifier cannot be nil")) } az.ID, err = randID() if err != nil { @@ -113,7 +88,7 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *types.Authorization) now := clock.Now() dbaz := &dbAuthz{ ID: az.ID, - AccountID: az.AccountId, + AccountID: az.AccountID, Status: types.StatusPending, Created: now, Expires: now.Add(defaultExpiryDuration), @@ -150,11 +125,14 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *types.Authorization) } dbaz.Challenges = chIDs - return db.saveDBAuthz(ctx, dbaz, nil) + return db.save(ctx, az.ID, dbaz, nil, "authz", authzTable) } // UpdateAuthorization saves an updated ACME Authorization to the database. func (db *DB) UpdateAuthorization(ctx context.Context, az *types.Authorization) error { + if len(az.ID) == 0 { + return ServerInternalErr(errors.New("id cannot be empty")) + } old, err := db.getDBAuthz(ctx, az.ID) if err != nil { return err @@ -164,53 +142,5 @@ func (db *DB) UpdateAuthorization(ctx context.Context, az *types.Authorization) nu.Status = az.Status nu.Error = az.Error - return db.saveDBAuthz(ctx, nu, old) -} - -/* -// updateStatus attempts to update the status on a dbAuthz and stores the -// updating object if necessary. -func (ba *dbAuthz) updateStatus(db nosql.DB) (authz, error) { - newAuthz := ba.clone() - - now := time.Now().UTC() - switch ba.Status { - case StatusInvalid: - return ba.parent(), nil - case StatusValid: - return ba.parent(), nil - case StatusPending: - // check expiry - if now.After(ba.Expires) { - newAuthz.Status = StatusInvalid - newAuthz.Error = MalformedErr(errors.New("authz has expired")) - break - } - - var isValid = false - for _, chID := range ba.Challenges { - ch, err := getChallenge(db, chID) - if err != nil { - return ba, err - } - if ch.getStatus() == StatusValid { - isValid = true - break - } - } - - if !isValid { - return ba.parent(), nil - } - newAuthz.Status = StatusValid - newAuthz.Error = nil - default: - return nil, ServerInternalErr(errors.Errorf("unrecognized authz status: %s", ba.Status)) - } - - if err := newAuthz.save(db, ba); err != nil { - return ba, err - } - return newAuthz.parent(), nil + return db.save(ctx, old.ID, nu, old, "authz", authzTable) } -*/ diff --git a/acme/db/nosql/challenge.go b/acme/db/nosql/challenge.go index 6303f005..bd3be0d0 100644 --- a/acme/db/nosql/challenge.go +++ b/acme/db/nosql/challenge.go @@ -35,39 +35,6 @@ func (dbc *dbChallenge) clone() *dbChallenge { return &u } -// save writes the challenge to disk. For new challenges 'old' should be nil, -// otherwise 'old' should be a pointer to the acme challenge as it was at the -// start of the request. This method will fail if the value currently found -// in the bucket/row does not match the value of 'old'. -func (db *DB) saveDBChallenge(ctx context.Context, nu challenge, old challenge) error { - newB, err := json.Marshal(nu) - if err != nil { - return ServerInternalErr(errors.Wrap(err, - "error marshaling new acme challenge")) - } - var oldB []byte - if old == nil { - oldB = nil - } else { - oldB, err = json.Marshal(old) - if err != nil { - return ServerInternalErr(errors.Wrap(err, - "error marshaling old acme challenge")) - } - } - - _, swapped, err := db.CmpAndSwap(challengeTable, []byte(bc.ID), oldB, newB) - switch { - case err != nil: - return ServerInternalErr(errors.Wrap(err, "error saving acme challenge")) - case !swapped: - return ServerInternalErr(errors.New("error saving acme challenge; " + - "acme challenge has changed since last read")) - default: - return nil - } -} - func (db *DB) getDBChallenge(ctx context.Context, id string) (*dbChallenge, error) { data, err := db.db.Get(challengeTable, []byte(id)) if nosql.IsErrNotFound(err) { @@ -121,7 +88,7 @@ func (db *DB) CreateChallenge(ctx context.context, ch *types.Challenge) error { Type: ch.Type, } - return dbch.saveDBChallenge(ctx, dbch, nil) + return dbch.save(ctx, ch.ID, dbch, nil, "challenge", challengeTable) } // GetChallenge retrieves and unmarshals an ACME challenge type from the database. @@ -149,7 +116,10 @@ func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*types.Chal // UpdateChallenge updates an ACME challenge type in the database. func (db *DB) UpdateChallenge(ctx context.Context, ch *types.Challenge) error { - old, err := db.getDBChallenge(ctx, id) + if len(ch.ID) == 0 { + return ServerInternalErr(errors.New("id cannot be empty")) + } + old, err := db.getDBChallenge(ctx, ch.ID) if err != nil { return err } @@ -163,366 +133,5 @@ func (db *DB) UpdateChallenge(ctx context.Context, ch *types.Challenge) error { nu.Validated = clock.Now() } - return db.saveDBChallenge(ctx, nu, old) + return db.save(ctx, old.ID, nu, old, "challenge", challengeTable) } - -//// http01Challenge represents an http-01 acme challenge. -//type http01Challenge struct { -// *baseChallenge -//} -// -//// newHTTP01Challenge returns a new acme http-01 challenge. -//func newHTTP01Challenge(db nosql.DB, ops ChallengeOptions) (challenge, error) { -// bc, err := newBaseChallenge(ops.AccountID, ops.AuthzID) -// if err != nil { -// return nil, err -// } -// bc.Type = "http-01" -// bc.Value = ops.Identifier.Value -// -// hc := &http01Challenge{bc} -// if err := hc.save(db, nil); err != nil { -// return nil, err -// } -// return hc, nil -//} -// -//// Validate attempts to validate the challenge. If the challenge has been -//// satisfactorily validated, the 'status' and 'validated' attributes are -//// updated. -//func (hc *http01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) { -// // If already valid or invalid then return without performing validation. -// if hc.getStatus() == StatusValid || hc.getStatus() == StatusInvalid { -// return hc, nil -// } -// url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", hc.Value, hc.Token) -// -// resp, err := vo.httpGet(url) -// if err != nil { -// if err = hc.storeError(db, ConnectionErr(errors.Wrapf(err, -// "error doing http GET for url %s", url))); err != nil { -// return nil, err -// } -// return hc, nil -// } -// if resp.StatusCode >= 400 { -// if err = hc.storeError(db, -// ConnectionErr(errors.Errorf("error doing http GET for url %s with status code %d", -// url, resp.StatusCode))); err != nil { -// return nil, err -// } -// return hc, nil -// } -// defer resp.Body.Close() -// -// body, err := ioutil.ReadAll(resp.Body) -// if err != nil { -// return nil, ServerInternalErr(errors.Wrapf(err, "error reading "+ -// "response body for url %s", url)) -// } -// keyAuth := strings.Trim(string(body), "\r\n") -// -// expected, err := KeyAuthorization(hc.Token, jwk) -// if err != nil { -// return nil, err -// } -// if keyAuth != expected { -// if err = hc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("keyAuthorization does not match; "+ -// "expected %s, but got %s", expected, keyAuth))); err != nil { -// return nil, err -// } -// return hc, nil -// } -// -// // Update and store the challenge. -// upd := &http01Challenge{hc.baseChallenge.clone()} -// upd.Status = StatusValid -// upd.Error = nil -// upd.Validated = clock.Now() -// -// if err := upd.save(db, hc); err != nil { -// return nil, err -// } -// return upd, nil -//} -// -//type tlsALPN01Challenge struct { -// *baseChallenge -//} -// -//// newTLSALPN01Challenge returns a new acme tls-alpn-01 challenge. -//func newTLSALPN01Challenge(db nosql.DB, ops ChallengeOptions) (challenge, error) { -// bc, err := newBaseChallenge(ops.AccountID, ops.AuthzID) -// if err != nil { -// return nil, err -// } -// bc.Type = "tls-alpn-01" -// bc.Value = ops.Identifier.Value -// -// hc := &tlsALPN01Challenge{bc} -// if err := hc.save(db, nil); err != nil { -// return nil, err -// } -// return hc, nil -//} -// -//func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) { -// // If already valid or invalid then return without performing validation. -// if tc.getStatus() == StatusValid || tc.getStatus() == StatusInvalid { -// return tc, nil -// } -// -// config := &tls.Config{ -// NextProtos: []string{"acme-tls/1"}, -// ServerName: tc.Value, -// InsecureSkipVerify: true, // we expect a self-signed challenge certificate -// } -// -// hostPort := net.JoinHostPort(tc.Value, "443") -// -// conn, err := vo.tlsDial("tcp", hostPort, config) -// if err != nil { -// if err = tc.storeError(db, -// ConnectionErr(errors.Wrapf(err, "error doing TLS dial for %s", hostPort))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// defer conn.Close() -// -// cs := conn.ConnectionState() -// certs := cs.PeerCertificates -// -// if len(certs) == 0 { -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("%s challenge for %s resulted in no certificates", -// tc.Type, tc.Value))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// -// if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != "acme-tls/1" { -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("cannot negotiate ALPN acme-tls/1 protocol for "+ -// "tls-alpn-01 challenge"))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// -// leafCert := certs[0] -// -// if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], tc.Value) { -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ -// "leaf certificate must contain a single DNS name, %v", tc.Value))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// -// 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) -// if err != nil { -// return nil, err -// } -// hashedKeyAuth := sha256.Sum256([]byte(keyAuth)) -// -// for _, ext := range leafCert.Extensions { -// if idPeAcmeIdentifier.Equal(ext.Id) { -// if !ext.Critical { -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ -// "acmeValidationV1 extension not critical"))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// -// var extValue []byte -// rest, err := asn1.Unmarshal(ext.Value, &extValue) -// -// if err != nil || len(rest) > 0 || len(hashedKeyAuth) != len(extValue) { -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ -// "malformed acmeValidationV1 extension value"))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// -// if subtle.ConstantTimeCompare(hashedKeyAuth[:], extValue) != 1 { -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ -// "expected acmeValidationV1 extension value %s for this challenge but got %s", -// hex.EncodeToString(hashedKeyAuth[:]), hex.EncodeToString(extValue)))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// -// upd := &tlsALPN01Challenge{tc.baseChallenge.clone()} -// upd.Status = StatusValid -// upd.Error = nil -// upd.Validated = clock.Now() -// -// if err := upd.save(db, tc); err != nil { -// return nil, err -// } -// return upd, nil -// } -// -// if idPeAcmeIdentifierV1Obsolete.Equal(ext.Id) { -// foundIDPeAcmeIdentifierV1Obsolete = true -// } -// } -// -// if foundIDPeAcmeIdentifierV1Obsolete { -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ -// "obsolete id-pe-acmeIdentifier in acmeValidationV1 extension"))); err != nil { -// return nil, err -// } -// return tc, nil -// } -// -// if err = tc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+ -// "missing acmeValidationV1 extension"))); err != nil { -// return nil, err -// } -// return tc, nil -//} -// -//// dns01Challenge represents an dns-01 acme challenge. -//type dns01Challenge struct { -// *baseChallenge -//} -// -//// newDNS01Challenge returns a new acme dns-01 challenge. -//func newDNS01Challenge(db nosql.DB, ops ChallengeOptions) (challenge, error) { -// bc, err := newBaseChallenge(ops.AccountID, ops.AuthzID) -// if err != nil { -// return nil, err -// } -// bc.Type = "dns-01" -// bc.Value = ops.Identifier.Value -// -// dc := &dns01Challenge{bc} -// if err := dc.save(db, nil); err != nil { -// return nil, err -// } -// return dc, nil -//} -// -//// KeyAuthorization creates the ACME key authorization value from a token -//// and a jwk. -//func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) { -// thumbprint, err := jwk.Thumbprint(crypto.SHA256) -// if err != nil { -// return "", ServerInternalErr(errors.Wrap(err, "error generating JWK thumbprint")) -// } -// encPrint := base64.RawURLEncoding.EncodeToString(thumbprint) -// return fmt.Sprintf("%s.%s", token, encPrint), nil -//} -// -//// validate attempts to validate the challenge. If the challenge has been -//// satisfactorily validated, the 'status' and 'validated' attributes are -//// updated. -//func (dc *dns01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) { -// // If already valid or invalid then return without performing validation. -// if dc.getStatus() == StatusValid || dc.getStatus() == StatusInvalid { -// return dc, nil -// } -// -// // 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, "*.") -// -// txtRecords, err := vo.lookupTxt("_acme-challenge." + domain) -// if err != nil { -// if err = dc.storeError(db, -// DNSErr(errors.Wrapf(err, "error looking up TXT "+ -// "records for domain %s", domain))); err != nil { -// return nil, err -// } -// return dc, nil -// } -// -// expectedKeyAuth, err := KeyAuthorization(dc.Token, jwk) -// if err != nil { -// return nil, err -// } -// h := sha256.Sum256([]byte(expectedKeyAuth)) -// expected := base64.RawURLEncoding.EncodeToString(h[:]) -// var found bool -// for _, r := range txtRecords { -// if r == expected { -// found = true -// break -// } -// } -// if !found { -// if err = dc.storeError(db, -// RejectedIdentifierErr(errors.Errorf("keyAuthorization "+ -// "does not match; expected %s, but got %s", expectedKeyAuth, txtRecords))); err != nil { -// return nil, err -// } -// return dc, nil -// } -// -// // Update and store the challenge. -// upd := &dns01Challenge{dc.baseChallenge.clone()} -// upd.Status = StatusValid -// upd.Error = nil -// upd.Validated = time.Now().UTC() -// -// if err := upd.save(db, dc); err != nil { -// return nil, err -// } -// return upd, nil -//} -// -//// unmarshalChallenge unmarshals a challenge type into the correct sub-type. -//func unmarshalChallenge(data []byte) (challenge, error) { -// var getType struct { -// Type string `json:"type"` -// } -// if err := json.Unmarshal(data, &getType); err != nil { -// return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling challenge type")) -// } -// -// switch getType.Type { -// case "dns-01": -// var bc baseChallenge -// if err := json.Unmarshal(data, &bc); err != nil { -// return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling "+ -// "challenge type into dns01Challenge")) -// } -// return &dns01Challenge{&bc}, nil -// case "http-01": -// var bc baseChallenge -// if err := json.Unmarshal(data, &bc); err != nil { -// return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling "+ -// "challenge type into http01Challenge")) -// } -// return &http01Challenge{&bc}, nil -// case "tls-alpn-01": -// var bc baseChallenge -// if err := json.Unmarshal(data, &bc); err != nil { -// return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling "+ -// "challenge type into tlsALPN01Challenge")) -// } -// return &tlsALPN01Challenge{&bc}, nil -// default: -// return nil, ServerInternalErr(errors.Errorf("unexpected challenge type '%s'", getType.Type)) -// } -//} -// diff --git a/acme/db/nosql/nonce.go b/acme/db/nosql/nonce.go index 3459f212..f8f57f89 100644 --- a/acme/db/nosql/nonce.go +++ b/acme/db/nosql/nonce.go @@ -33,16 +33,10 @@ func (db *DB) CreateNonce() (Nonce, error) { if err != nil { return nil, ServerInternalErr(errors.Wrap(err, "error marshaling nonce")) } - _, swapped, err := db.CmpAndSwap(nonceTable, []byte(id), nil, b) - switch { - case err != nil: - return nil, ServerInternalErr(errors.Wrap(err, "error storing nonce")) - case !swapped: - return nil, ServerInternalErr(errors.New("error storing nonce; " + - "value has changed since last read")) - default: - return Nonce(id), nil + if err = db.save(ctx, id, b, nil, "nonce", nonceTable); err != nil { + return "", err } + return Nonce(id), nil } // DeleteNonce verifies that the nonce is valid (by checking if it exists), diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 8bfd1a66..e11b92b2 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -1,10 +1,56 @@ package nosql import ( + "context" + "encoding/json" + + "github.com/pkg/errors" nosqlDB "github.com/smallstep/nosql" ) +var ( + accountTable = []byte("acme_accounts") + accountByKeyIDTable = []byte("acme_keyID_accountID_index") + authzTable = []byte("acme_authzs") + challengeTable = []byte("acme_challenges") + nonceTable = []byte("nonces") + orderTable = []byte("acme_orders") + ordersByAccountIDTable = []byte("acme_account_orders_index") + certTable = []byte("acme_certs") +) + // DB is a struct that implements the AcmeDB interface. type DB struct { db nosqlDB.DB } + +// 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)) + } + var oldB []byte + if old == nil { + oldB = nil + } else { + oldB, err = json.Marshal(old) + if err != nil { + return ServerInternalErr(errors.Wrapf(err, + "error marshaling old acme %s", typ)) + } + } + + _, swapped, err := db.CmpAndSwap(table, []byte(id), oldB, newB) + switch { + case err != nil: + return ServerInternalErr(errors.Wrapf(err, "error saving acme %s", typ)) + case !swapped: + return ServerInternalErr(errors.Errorf("error saving acme %s; "+ + "changed since last read", typ)) + default: + return nil + } +} diff --git a/acme/db/nosql/order.go b/acme/db/nosql/order.go index 5ba54790..a0ab60da 100644 --- a/acme/db/nosql/order.go +++ b/acme/db/nosql/order.go @@ -2,17 +2,13 @@ package nosql import ( "context" - "crypto/x509" "encoding/json" - "sort" - "strings" "sync" "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/acme" "github.com/smallstep/nosql" - "go.step.sm/crypto/x509util" ) var defaultOrderExpiry = time.Hour * 24 @@ -20,19 +16,10 @@ var defaultOrderExpiry = time.Hour * 24 // Mutex for locking ordersByAccount index operations. var ordersByAccountMux sync.Mutex -// OrderOptions options with which to create a new Order. -type OrderOptions struct { - AccountID string `json:"accID"` - Identifiers []Identifier `json:"identifiers"` - NotBefore time.Time `json:"notBefore"` - NotAfter time.Time `json:"notAfter"` - backdate time.Duration - defaultDuration time.Duration -} - 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"` @@ -60,7 +47,7 @@ func (db *DB) getDBOrder(id string) (*dbOrder, error) { } // GetOrder retrieves an ACME Order from the database. -func (db *DB) GetOrder(id string) (*types.Order, error) { +func (db *DB) GetOrder(id string) (*acme.Order, error) { dbo, err := db.getDBOrder(id) azs := make([]string, len(dbo.Authorizations)) @@ -76,6 +63,7 @@ func (db *DB) GetOrder(id string) (*types.Order, error) { Authorizations: azs, Finalize: dir.getLink(ctx, FinalizeLink, true, o.ID), ID: dbo.ID, + ProvisionerID: dbo.ProvisionerID, } if dbo.Certificate != "" { @@ -84,60 +72,74 @@ func (db *DB) GetOrder(id string) (*types.Order, error) { return o, nil } -func (db *DB) CreateOrder(ctx context.Context, o *types.Order) error { - o.ID, err := randID() +// 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 } - authzs := make([]string, len(ops.Identifiers)) + azIDs := make([]string, len(ops.Identifiers)) for i, identifier := range ops.Identifiers { - az, err := newAuthz(db, ops.AccountID, identifier) + az, err = db.CreateAuthorzation(&types.Authorization{ + AccountID: o.AccountID, + Identifier: o.Identifier, + }) if err != nil { - return nil, err + return err } - authzs[i] = az.getID() + azIDs[i] = az.ID } now := clock.Now() var backdate time.Duration - nbf := ops.NotBefore + nbf := o.NotBefore if nbf.IsZero() { nbf = now - backdate = -1 * ops.backdate + backdate = -1 * o.Backdate } - naf := ops.NotAfter + naf := o.NotAfter if naf.IsZero() { - naf = nbf.Add(ops.defaultDuration) + naf = nbf.Add(o.DefaultDuration) } dbo := &dbOrder{ - ID: id, - AccountID: ops.AccountID, + ID: o.ID, + AccountID: o.AccountID, + ProvisionerID: o.ProvisionerID, Created: now, Status: StatusPending, Expires: now.Add(defaultOrderExpiry), Identifiers: ops.Identifiers, NotBefore: nbf.Add(backdate), NotAfter: naf, - Authorizations: authzs, + Authorizations: azIDs, } - if err := db.saveDBOrder(dbo, nil); err != nil { + if err := db.save(ctx, o.ID, dbo, nil, orderTable); err != nil { return nil, err } var oidHelper = orderIDsByAccount{} - _, err = oidHelper.addOrderID(db, ops.AccountID, o.ID) + _, err = oidHelper.addOrderID(db, o.AccountID, o.ID) if err != nil { return nil, err } return o, nil } -// newOrder returns a new Order type. -func newOrder(db nosql.DB, ops OrderOptions) (*order, error) { -} - type orderIDsByAccount struct{} // addOrderID adds an order ID to a users index of in progress order IDs. @@ -188,7 +190,7 @@ func (oiba orderIDsByAccount) unsafeGetOrderIDsByAccount(db nosql.DB, accID stri if err != nil { return nil, ServerInternalErr(errors.Wrapf(err, "error loading order %s for account %s", oid, accID)) } - if o, err = o.updateStatus(db); err != nil { + if o, err = o.UpdateStatus(db); err != nil { return nil, ServerInternalErr(errors.Wrapf(err, "error updating order %s for account %s", oid, accID)) } if o.Status == StatusPending { @@ -198,7 +200,7 @@ func (oiba orderIDsByAccount) unsafeGetOrderIDsByAccount(db nosql.DB, accID stri // If the number of pending orders is less than the number of orders in the // list, then update the pending order list. if len(pendOids) != len(oids) { - if err = orderIDs(pendOids).save(db, oids, accID); err != nil { + if err = orderIDs(pendOiUs).save(db, oids, accID); err != nil { return nil, ServerInternalErr(errors.Wrapf(err, "error storing orderIDs as part of getOrderIDsByAccount logic: "+ "len(orderIDs) = %d", len(pendOids))) } @@ -248,225 +250,3 @@ func (oids orderIDs) save(db nosql.DB, old orderIDs, accID string) error { return nil } } - -func (o *order) save(db nosql.DB, old *order) error { - var ( - err error - oldB []byte - ) - if old == nil { - oldB = nil - } else { - if oldB, err = json.Marshal(old); err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling old acme order")) - } - } - - newB, err := json.Marshal(o) - if err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling new acme order")) - } - - _, swapped, err := db.CmpAndSwap(orderTable, []byte(o.ID), oldB, newB) - switch { - case err != nil: - return ServerInternalErr(errors.Wrap(err, "error storing order")) - case !swapped: - return ServerInternalErr(errors.New("error storing order; " + - "value has changed since last read")) - default: - return nil - } -} - -// updateStatus updates order status if necessary. -func (o *order) updateStatus(db nosql.DB) (*order, error) { - _newOrder := *o - newOrder := &_newOrder - - now := time.Now().UTC() - switch o.Status { - case StatusInvalid: - return o, nil - case StatusValid: - return o, nil - case StatusReady: - // check expiry - if now.After(o.Expires) { - newOrder.Status = StatusInvalid - newOrder.Error = MalformedErr(errors.New("order has expired")) - break - } - return o, nil - case StatusPending: - // check expiry - if now.After(o.Expires) { - newOrder.Status = StatusInvalid - newOrder.Error = MalformedErr(errors.New("order has expired")) - break - } - - var count = map[string]int{ - StatusValid: 0, - StatusInvalid: 0, - StatusPending: 0, - } - for _, azID := range o.Authorizations { - az, err := getAuthz(db, azID) - if err != nil { - return nil, err - } - if az, err = az.updateStatus(db); err != nil { - return nil, err - } - st := az.getStatus() - count[st]++ - } - switch { - case count[StatusInvalid] > 0: - newOrder.Status = StatusInvalid - - // No change in the order status, so just return the order as is - - // without writing any changes. - case count[StatusPending] > 0: - return newOrder, nil - - case count[StatusValid] == len(o.Authorizations): - newOrder.Status = StatusReady - - default: - return nil, ServerInternalErr(errors.New("unexpected authz status")) - } - default: - return nil, ServerInternalErr(errors.Errorf("unrecognized order status: %s", o.Status)) - } - - if err := newOrder.save(db, o); err != nil { - return nil, err - } - return newOrder, nil -} - -// finalize signs a certificate if the necessary conditions for Order completion -// have been met. -func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAuthority, p Provisioner) (*order, error) { - var err error - if o, err = o.updateStatus(db); err != nil { - return nil, err - } - - switch o.Status { - case StatusInvalid: - return nil, OrderNotReadyErr(errors.Errorf("order %s has been abandoned", o.ID)) - case StatusValid: - return o, nil - case StatusPending: - return nil, OrderNotReadyErr(errors.Errorf("order %s is not ready", o.ID)) - case StatusReady: - break - default: - return nil, ServerInternalErr(errors.Errorf("unexpected status %s for order %s", o.Status, o.ID)) - } - - // RFC8555: The CSR MUST indicate the exact same set of requested - // identifiers as the initial newOrder request. Identifiers of type "dns" - // MUST appear either in the commonName portion of the requested subject - // name or in an extensionRequest attribute [RFC2985] requesting a - // subjectAltName extension, or both. - if csr.Subject.CommonName != "" { - csr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName) - } - csr.DNSNames = uniqueLowerNames(csr.DNSNames) - orderNames := make([]string, len(o.Identifiers)) - for i, n := range o.Identifiers { - orderNames[i] = n.Value - } - orderNames = uniqueLowerNames(orderNames) - - // Validate identifier names against CSR alternative names. - // - // Note that with certificate templates we are not going to check for the - // 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)) - } - - 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)) - } - sans[i] = x509util.SubjectAlternativeName{ - Type: x509util.DNSType, - Value: csr.DNSNames[i], - } - } - - // Get authorizations from the ACME provisioner. - ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod) - signOps, err := p.AuthorizeSign(ctx, "") - if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error retrieving authorization options from ACME provisioner")) - } - - // Template data - data := x509util.NewTemplateData() - data.SetCommonName(csr.Subject.CommonName) - data.Set(x509util.SANsKey, sans) - - templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) - if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error creating template options from ACME provisioner")) - } - signOps = append(signOps, templateOptions) - - // Create and store a new certificate. - certChain, err := auth.Sign(csr, provisioner.SignOptions{ - NotBefore: provisioner.NewTimeDuration(o.NotBefore), - NotAfter: provisioner.NewTimeDuration(o.NotAfter), - }, signOps...) - if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error generating certificate for order %s", o.ID)) - } - - cert, err := newCert(db, CertOptions{ - AccountID: o.AccountID, - OrderID: o.ID, - Leaf: certChain[0], - Intermediates: certChain[1:], - }) - if err != nil { - return nil, err - } - - _newOrder := *o - newOrder := &_newOrder - newOrder.Certificate = cert.ID - newOrder.Status = StatusValid - if err := newOrder.save(db, o); err != nil { - return nil, err - } - return newOrder, nil -} - -// toACME converts the internal Order type into the public acmeOrder type for -// presentation in the ACME protocol. -func (o *order) toACME(ctx context.Context, db nosql.DB, dir *directory) (*Order, error) { -} - -// uniqueLowerNames returns the set of all unique names in the input after all -// of them are lowercased. The returned names will be in their lowercased form -// and sorted alphabetically. -func uniqueLowerNames(names []string) (unique []string) { - nameMap := make(map[string]int, len(names)) - for _, name := range names { - nameMap[strings.ToLower(name)] = 1 - } - unique = make([]string, 0, len(nameMap)) - for name := range nameMap { - unique = append(unique, name) - } - sort.Strings(unique) - return -} diff --git a/acme/errors.go b/acme/errors.go index a4dd8159..9bd9c400 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -186,6 +186,9 @@ func RejectedIdentifierErr(err error) *Error { // 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", diff --git a/acme/types/nonce.go b/acme/nonce.go similarity index 100% rename from acme/types/nonce.go rename to acme/nonce.go diff --git a/acme/order.go b/acme/order.go index 574477ca..16a0ead2 100644 --- a/acme/order.go +++ b/acme/order.go @@ -1,4 +1,3 @@ -package acme import ( "context" @@ -6,32 +5,28 @@ import ( "encoding/json" "sort" "strings" - "sync" "time" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" - "github.com/smallstep/nosql" "go.step.sm/crypto/x509util" ) -var defaultOrderExpiry = time.Hour * 24 - -// Mutex for locking ordersByAccount index operations. -var ordersByAccountMux sync.Mutex - // Order contains order metadata for the ACME protocol order type. type Order struct { - Status string `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"` - Certificate string `json:"certificate,omitempty"` - ID string `json:"-"` + Status string `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"` + Certificate string `json:"certificate,omitempty"` + ID string `json:"-"` + ProvisionerID string `json:"-"` + DefaultDuration time.Duration `json:"-"` + Backdate time.Duration `json:"-"` } // ToLog enables response logging. @@ -43,251 +38,33 @@ func (o *Order) ToLog() (interface{}, error) { return string(b), nil } -// GetID returns the Order ID. -func (o *Order) GetID() string { - return o.ID -} - -// OrderOptions options with which to create a new Order. -type OrderOptions struct { - AccountID string `json:"accID"` - Identifiers []Identifier `json:"identifiers"` - NotBefore time.Time `json:"notBefore"` - NotAfter time.Time `json:"notAfter"` - backdate time.Duration - defaultDuration time.Duration -} - -type order struct { - ID string `json:"id"` - AccountID string `json:"accountID"` - 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"` -} - -// newOrder returns a new Order type. -func newOrder(db nosql.DB, ops OrderOptions) (*order, error) { - id, err := randID() - if err != nil { - return nil, err - } - - authzs := make([]string, len(ops.Identifiers)) - for i, identifier := range ops.Identifiers { - az, err := newAuthz(db, ops.AccountID, identifier) - if err != nil { - return nil, err - } - authzs[i] = az.getID() - } - - now := clock.Now() - var backdate time.Duration - nbf := ops.NotBefore - if nbf.IsZero() { - nbf = now - backdate = -1 * ops.backdate - } - naf := ops.NotAfter - if naf.IsZero() { - naf = nbf.Add(ops.defaultDuration) - } - - o := &order{ - ID: id, - AccountID: ops.AccountID, - Created: now, - Status: StatusPending, - Expires: now.Add(defaultOrderExpiry), - Identifiers: ops.Identifiers, - NotBefore: nbf.Add(backdate), - NotAfter: naf, - Authorizations: authzs, - } - if err := o.save(db, nil); err != nil { - return nil, err - } - - var oidHelper = orderIDsByAccount{} - _, err = oidHelper.addOrderID(db, ops.AccountID, o.ID) - if err != nil { - return nil, err - } - return o, nil -} - -type orderIDsByAccount struct{} - -// addOrderID adds an order ID to a users index of in progress order IDs. -// This method will also cull any orders that are no longer in the `pending` -// state from the index before returning it. -func (oiba orderIDsByAccount) addOrderID(db nosql.DB, accID string, oid string) ([]string, error) { - ordersByAccountMux.Lock() - defer ordersByAccountMux.Unlock() - - // Update the "order IDs by account ID" index - oids, err := oiba.unsafeGetOrderIDsByAccount(db, accID) - if err != nil { - return nil, err - } - newOids := append(oids, oid) - if err = orderIDs(newOids).save(db, oids, accID); err != nil { - // Delete the entire order if storing the index fails. - db.Del(orderTable, []byte(oid)) - return nil, err - } - return newOids, nil -} - -// unsafeGetOrderIDsByAccount retrieves a list of Order IDs that were created by the -// account. -func (oiba orderIDsByAccount) unsafeGetOrderIDsByAccount(db nosql.DB, accID string) ([]string, error) { - b, err := db.Get(ordersByAccountIDTable, []byte(accID)) - if err != nil { - if nosql.IsErrNotFound(err) { - return []string{}, nil - } - return nil, ServerInternalErr(errors.Wrapf(err, "error loading orderIDs for account %s", accID)) - } - var oids []string - if err := json.Unmarshal(b, &oids); err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error unmarshaling orderIDs for account %s", accID)) - } - - // Remove any order that is not in PENDING state and update the stored list - // before returning. - // - // According to RFC 8555: - // The server SHOULD include pending orders and SHOULD NOT include orders - // that are invalid in the array of URLs. - pendOids := []string{} - for _, oid := range oids { - o, err := getOrder(db, oid) - if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error loading order %s for account %s", oid, accID)) - } - if o, err = o.updateStatus(db); err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error updating order %s for account %s", oid, accID)) - } - if o.Status == StatusPending { - pendOids = append(pendOids, oid) - } - } - // If the number of pending orders is less than the number of orders in the - // list, then update the pending order list. - if len(pendOids) != len(oids) { - if err = orderIDs(pendOids).save(db, oids, accID); err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error storing orderIDs as part of getOrderIDsByAccount logic: "+ - "len(orderIDs) = %d", len(pendOids))) - } - } - - return pendOids, nil -} - -type orderIDs []string - -// save is used to update the list of orderIDs keyed by ACME account ID -// stored in the database. -// -// This method always converts empty lists to 'nil' when storing to the DB. We -// do this to avoid any confusion between an empty list and a nil value in the -// db. -func (oids orderIDs) save(db nosql.DB, old orderIDs, accID string) error { - var ( - err error - oldb []byte - newb []byte - ) - if len(old) == 0 { - oldb = nil - } else { - oldb, err = json.Marshal(old) - if err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling old order IDs slice")) - } - } - if len(oids) == 0 { - newb = nil - } else { - newb, err = json.Marshal(oids) - if err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling new order IDs slice")) - } - } - _, swapped, err := db.CmpAndSwap(ordersByAccountIDTable, []byte(accID), oldb, newb) - switch { - case err != nil: - return ServerInternalErr(errors.Wrapf(err, "error storing order IDs for account %s", accID)) - case !swapped: - return ServerInternalErr(errors.Errorf("error storing order IDs "+ - "for account %s; order IDs changed since last read", accID)) - default: - return nil - } -} - -func (o *order) save(db nosql.DB, old *order) error { - var ( - err error - oldB []byte - ) - if old == nil { - oldB = nil - } else { - if oldB, err = json.Marshal(old); err != nil { - return ServerInternalErr(errors.Wrap(err, "error marshaling old acme order")) - } - } - - newB, err := json.Marshal(o) +// UpdateStatus updates the ACME Order Status if necessary. +// 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 ServerInternalErr(errors.Wrap(err, "error marshaling new acme order")) + return ServerInternalErr(errors.Wrap("error converting expiry string to time")) } - _, swapped, err := db.CmpAndSwap(orderTable, []byte(o.ID), oldB, newB) - switch { - case err != nil: - return ServerInternalErr(errors.Wrap(err, "error storing order")) - case !swapped: - return ServerInternalErr(errors.New("error storing order; " + - "value has changed since last read")) - default: - return nil - } -} - -// updateStatus updates order status if necessary. -func (o *order) updateStatus(db nosql.DB) (*order, error) { - _newOrder := *o - newOrder := &_newOrder - - now := time.Now().UTC() switch o.Status { case StatusInvalid: - return o, nil + return nil case StatusValid: - return o, nil + return nil case StatusReady: - // check expiry - if now.After(o.Expires) { - newOrder.Status = StatusInvalid - newOrder.Error = MalformedErr(errors.New("order has expired")) + // Check expiry + if now.After(expiry) { + o.Status = StatusInvalid + o.Error = MalformedErr(errors.New("order has expired")) break } - return o, nil + return nil case StatusPending: - // check expiry - if now.After(o.Expires) { - newOrder.Status = StatusInvalid - newOrder.Error = MalformedErr(errors.New("order has expired")) + // Check expiry + if now.After(expiry) { + o.Status = StatusInvalid + o.Error = MalformedErr(errors.New("order has expired")) break } @@ -297,27 +74,27 @@ func (o *order) updateStatus(db nosql.DB) (*order, error) { StatusPending: 0, } for _, azID := range o.Authorizations { - az, err := getAuthz(db, azID) + az, err := db.GetAuthorization(ctx, azID) if err != nil { - return nil, err + return false, err } - if az, err = az.updateStatus(db); err != nil { - return nil, err + if az, err = az.UpdateStatus(db); err != nil { + return false, err } - st := az.getStatus() + st := az.Status count[st]++ } switch { case count[StatusInvalid] > 0: - newOrder.Status = StatusInvalid + o.Status = StatusInvalid // No change in the order status, so just return the order as is - // without writing any changes. case count[StatusPending] > 0: - return newOrder, nil + return nil case count[StatusValid] == len(o.Authorizations): - newOrder.Status = StatusReady + o.Status = StatusReady default: return nil, ServerInternalErr(errors.New("unexpected authz status")) @@ -325,28 +102,24 @@ func (o *order) updateStatus(db nosql.DB) (*order, error) { default: return nil, ServerInternalErr(errors.Errorf("unrecognized order status: %s", o.Status)) } - - if err := newOrder.save(db, o); err != nil { - return nil, err - } - return newOrder, nil + return db.UpdateOrder(ctx, o) } // finalize signs a certificate if the necessary conditions for Order completion // have been met. -func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAuthority, p Provisioner) (*order, error) { +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 { + if o, err = o.UpdateStatus(db); err != nil { return nil, err } switch o.Status { case StatusInvalid: - return nil, OrderNotReadyErr(errors.Errorf("order %s has been abandoned", o.ID)) + return OrderNotReadyErr(errors.Errorf("order %s has been abandoned", o.ID)) case StatusValid: - return o, nil + return nil case StatusPending: - return nil, OrderNotReadyErr(errors.Errorf("order %s is not ready", o.ID)) + return OrderNotReadyErr(errors.Errorf("order %s is not ready", o.ID)) case StatusReady: break default: @@ -366,7 +139,7 @@ func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAut for i, n := range o.Identifiers { orderNames[i] = n.Value } - orderNames = uniqueLowerNames(orderNames) + orderNames = uniqueSortedLowerNames(orderNames) // Validate identifier names against CSR alternative names. // @@ -425,59 +198,15 @@ func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAut return nil, err } - _newOrder := *o - newOrder := &_newOrder - newOrder.Certificate = cert.ID - newOrder.Status = StatusValid - if err := newOrder.save(db, o); err != nil { - return nil, err - } - return newOrder, nil -} - -// getOrder retrieves and unmarshals an ACME Order type from the database. -func getOrder(db nosql.DB, id string) (*order, error) { - b, err := db.Get(orderTable, []byte(id)) - if nosql.IsErrNotFound(err) { - return nil, MalformedErr(errors.Wrapf(err, "order %s not found", id)) - } else if err != nil { - return nil, ServerInternalErr(errors.Wrapf(err, "error loading order %s", id)) - } - var o order - if err := json.Unmarshal(b, &o); err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling order")) - } - return &o, nil -} - -// toACME converts the internal Order type into the public acmeOrder type for -// presentation in the ACME protocol. -func (o *order) toACME(ctx context.Context, db nosql.DB, dir *directory) (*Order, error) { - azs := make([]string, len(o.Authorizations)) - for i, aid := range o.Authorizations { - azs[i] = dir.getLink(ctx, AuthzLink, true, aid) - } - ao := &Order{ - Status: o.Status, - Expires: o.Expires.Format(time.RFC3339), - Identifiers: o.Identifiers, - NotBefore: o.NotBefore.Format(time.RFC3339), - NotAfter: o.NotAfter.Format(time.RFC3339), - Authorizations: azs, - Finalize: dir.getLink(ctx, FinalizeLink, true, o.ID), - ID: o.ID, - } - - if o.Certificate != "" { - ao.Certificate = dir.getLink(ctx, CertificateLink, true, o.Certificate) - } - return ao, nil + o.Certificate = cert.ID + o.Status = StatusValid + return db.UpdateOrder(ctx, o) } -// uniqueLowerNames returns the set of all unique names in the input after all +// uniqueSortedLowerNames returns the set of all unique names in the input after all // of them are lowercased. The returned names will be in their lowercased form // and sorted alphabetically. -func uniqueLowerNames(names []string) (unique []string) { +func uniqueSortedLowerNames(names []string) (unique []string) { nameMap := make(map[string]int, len(names)) for _, name := range names { nameMap[strings.ToLower(name)] = 1 diff --git a/acme/types/status.go b/acme/status.go similarity index 100% rename from acme/types/status.go rename to acme/status.go diff --git a/acme/types/authz.go b/acme/types/authz.go deleted file mode 100644 index 4119f6c1..00000000 --- a/acme/types/authz.go +++ /dev/null @@ -1,27 +0,0 @@ -package types - -import ( - "encoding/json" - - "github.com/pkg/errors" -) - -// Authorization representst an ACME Authorization. -type Authorization struct { - Identifier Identifier `json:"identifier"` - Status string `json:"status"` - Expires string `json:"expires"` - Challenges []*Challenge `json:"challenges"` - Wildcard bool `json:"wildcard"` - ID string `json:"-"` - AccountID string `json:"-"` -} - -// ToLog enables response logging. -func (a *Authz) ToLog() (interface{}, error) { - b, err := json.Marshal(a) - if err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error marshaling authz for logging")) - } - return string(b), nil -} diff --git a/acme/types/challenge.go b/acme/types/challenge.go deleted file mode 100644 index 61bcd2fb..00000000 --- a/acme/types/challenge.go +++ /dev/null @@ -1,30 +0,0 @@ -package types - -import ( - "encoding/json" - - "github.com/pkg/errors" -) - -// Challenge represents an ACME response Challenge type. -type Challenge struct { - Type string `json:"type"` - Status string `json:"status"` - Token string `json:"token"` - Validated string `json:"validated,omitempty"` - URL string `json:"url"` - Error *AError `json:"error,omitempty"` - ID string `json:"-"` - AuthzID string `json:"-"` - AccountID string `json:"-"` - Value string `json:"-"` -} - -// ToLog enables response logging. -func (c *Challenge) ToLog() (interface{}, error) { - b, err := json.Marshal(c) - if err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error marshaling challenge for logging")) - } - return string(b), nil -} diff --git a/acme/types/order.go b/acme/types/order.go deleted file mode 100644 index 7e472204..00000000 --- a/acme/types/order.go +++ /dev/null @@ -1,31 +0,0 @@ -package types - -import ( - "encoding/json" - - "github.com/pkg/errors" -) - -// Order contains order metadata for the ACME protocol order type. -type Order struct { - Status string `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"` - Certificate string `json:"certificate,omitempty"` - ID string `json:"-"` - ProvisionerID string `json:"-"` -} - -// ToLog enables response logging. -func (o *Order) ToLog() (interface{}, error) { - b, err := json.Marshal(o) - if err != nil { - return nil, ServerInternalErr(errors.Wrap(err, "error marshaling order for logging")) - } - return string(b), nil -}