diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6da2aa27..5d0416ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: 'latest' + version: 'v1.44.0' # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 10e7360d..f36e78ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: 'v1.43.0' + version: 'v1.44.0' # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/CHANGELOG.md b/CHANGELOG.md index b41a90e3..9a0618ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased - 0.18.1] - DATE +## [Unreleased - 0.18.2] - DATE ### Added -- Support for ACME revocation. ### Changed +- IPv6 addresses are normalized as IP addresses instead of hostnames. +- More descriptive JWK decryption error message. ### Deprecated ### Removed ### Fixed ### Security +## [0.18.1] - 2022-02-03 +### Added +- Support for ACME revocation. +- Replace hash function with an RSA SSH CA to "rsa-sha2-256". +- Support Nebula provisioners. +- Example Ansible configurations. +- Support PKCS#11 as a decrypter, as used by SCEP. +### Changed +- Automatically create database directory on `step ca init`. +- Slightly improve errors reported when a template has invalid content. +- Error reporting in logs and to clients. +### Fixed +- SCEP renewal using HTTPS on macOS. + ## [0.18.0] - 2021-11-17 ### Added - Support for multiple certificate authority contexts. diff --git a/acme/account.go b/acme/account.go index 197a3400..027d7be1 100644 --- a/acme/account.go +++ b/acme/account.go @@ -4,6 +4,7 @@ import ( "crypto" "encoding/base64" "encoding/json" + "time" "go.step.sm/crypto/jose" ) @@ -11,11 +12,12 @@ import ( // Account is a subset of the internal account type containing only those // attributes required for responses in the ACME protocol. type Account struct { - ID string `json:"-"` - Key *jose.JSONWebKey `json:"-"` - Contact []string `json:"contact,omitempty"` - Status Status `json:"status"` - OrdersURL string `json:"orders"` + ID string `json:"-"` + Key *jose.JSONWebKey `json:"-"` + Contact []string `json:"contact,omitempty"` + Status Status `json:"status"` + OrdersURL string `json:"orders"` + ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"` } // ToLog enables response logging. @@ -40,3 +42,32 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) { } return base64.RawURLEncoding.EncodeToString(kid), nil } + +// ExternalAccountKey is an ACME External Account Binding key. +type ExternalAccountKey struct { + ID string `json:"id"` + ProvisionerID string `json:"provisionerID"` + Reference string `json:"reference"` + AccountID string `json:"-"` + KeyBytes []byte `json:"-"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt,omitempty"` +} + +// AlreadyBound returns whether this EAK is already bound to +// an ACME Account or not. +func (eak *ExternalAccountKey) AlreadyBound() bool { + return !eak.BoundAt.IsZero() +} + +// BindTo binds the EAK to an Account. +// It returns an error if it's already bound. +func (eak *ExternalAccountKey) BindTo(account *Account) error { + if eak.AlreadyBound() { + return NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", eak.ID, eak.AccountID, eak.BoundAt) + } + eak.AccountID = account.ID + eak.BoundAt = time.Now() + eak.KeyBytes = []byte{} // clearing the key bytes; can only be used once + return nil +} diff --git a/acme/account_test.go b/acme/account_test.go index 5625c3dc..33524d87 100644 --- a/acme/account_test.go +++ b/acme/account_test.go @@ -4,6 +4,7 @@ import ( "crypto" "encoding/base64" "testing" + "time" "github.com/pkg/errors" "github.com/smallstep/assert" @@ -79,3 +80,67 @@ func TestAccount_IsValid(t *testing.T) { }) } } + +func TestExternalAccountKey_BindTo(t *testing.T) { + boundAt := time.Now() + tests := []struct { + name string + eak *ExternalAccountKey + acct *Account + err *Error + }{ + { + name: "ok", + eak: &ExternalAccountKey{ + ID: "eakID", + ProvisionerID: "provID", + Reference: "ref", + KeyBytes: []byte{1, 3, 3, 7}, + }, + acct: &Account{ + ID: "accountID", + }, + err: nil, + }, + { + name: "fail/already-bound", + eak: &ExternalAccountKey{ + ID: "eakID", + ProvisionerID: "provID", + Reference: "ref", + KeyBytes: []byte{1, 3, 3, 7}, + AccountID: "someAccountID", + BoundAt: boundAt, + }, + acct: &Account{ + ID: "accountID", + }, + err: NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", "eakID", "someAccountID", boundAt), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eak := tt.eak + acct := tt.acct + err := eak.BindTo(acct) + wantErr := tt.err != nil + gotErr := err != nil + if wantErr != gotErr { + t.Errorf("ExternalAccountKey.BindTo() error = %v, wantErr %v", err, tt.err) + } + if wantErr { + assert.NotNil(t, err) + assert.Type(t, &Error{}, err) + ae, _ := err.(*Error) + assert.Equals(t, ae.Type, tt.err.Type) + assert.Equals(t, ae.Detail, tt.err.Detail) + assert.Equals(t, ae.Identifier, tt.err.Identifier) + assert.Equals(t, ae.Subproblems, tt.err.Subproblems) + } else { + assert.Equals(t, eak.AccountID, acct.ID) + assert.Equals(t, eak.KeyBytes, []byte{}) + assert.NotNil(t, eak.BoundAt) + } + }) + } +} diff --git a/acme/api/account.go b/acme/api/account.go index 259cb2a2..0dc8ab40 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -12,9 +12,10 @@ import ( // NewAccountRequest represents the payload for a new account request. type NewAccountRequest struct { - Contact []string `json:"contact"` - OnlyReturnExisting bool `json:"onlyReturnExisting"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + Contact []string `json:"contact"` + OnlyReturnExisting bool `json:"onlyReturnExisting"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + ExternalAccountBinding *ExternalAccountBinding `json:"externalAccountBinding,omitempty"` } func validateContacts(cs []string) error { @@ -83,8 +84,14 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } + prov, err := acmeProvisionerFromContext(ctx) + if err != nil { + api.WriteError(w, err) + return + } + httpStatus := http.StatusCreated - acc, err := accountFromContext(r.Context()) + acc, err := accountFromContext(ctx) if err != nil { acmeErr, ok := err.(*acme.Error) if !ok || acmeErr.Status != http.StatusBadRequest { @@ -99,12 +106,19 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { "account does not exist")) return } + jwk, err := jwkFromContext(ctx) if err != nil { api.WriteError(w, err) return } + eak, err := h.validateExternalAccountBinding(ctx, &nar) + if err != nil { + api.WriteError(w, err) + return + } + acc = &acme.Account{ Key: jwk, Contact: nar.Contact, @@ -114,8 +128,21 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { api.WriteError(w, acme.WrapErrorISE(err, "error creating account")) return } + + if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response + err := eak.BindTo(acc) + if err != nil { + api.WriteError(w, err) + return + } + if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { + api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key")) + return + } + acc.ExternalAccountBinding = nar.ExternalAccountBinding + } } else { - // Account exists // + // Account exists httpStatus = http.StatusOK } diff --git a/acme/api/account_test.go b/acme/api/account_test.go index abee97a2..4c3404ec 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/go-chi/chi" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" @@ -40,6 +41,66 @@ func newProv() acme.Provisioner { return p } +func newACMEProv(t *testing.T) *provisioner.ACME { + p := newProv() + a, ok := p.(*provisioner.ACME) + if !ok { + t.Fatal("not a valid ACME provisioner") + } + return a +} + +func createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) (*jose.JSONWebSignature, error) { + signer, err := jose.NewSigner( + jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm("HS256"), + Key: hmacKey, + }, + &jose.SignerOptions{ + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": keyID, + "url": u, + }, + EmbedJWK: false, + }, + ) + if err != nil { + return nil, err + } + + jwkJSONBytes, err := jwk.Public().MarshalJSON() + if err != nil { + return nil, err + } + + jws, err := signer.Sign(jwkJSONBytes) + if err != nil { + return nil, err + } + + raw, err := jws.CompactSerialize() + if err != nil { + return nil, err + } + + parsedJWS, err := jose.ParseJWS(raw) + if err != nil { + return nil, err + } + + return parsedJWS, nil +} + +func createRawEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) ([]byte, error) { + jws, err := createEABJWS(jwk, hmacKey, keyID, u) + if err != nil { + return nil, err + } + + rawJWS := jws.FullSerialize() + return []byte(rawJWS), nil +} + func TestNewAccountRequest_Validate(t *testing.T) { type test struct { nar *NewAccountRequest @@ -290,6 +351,7 @@ func TestHandler_NewAccount(t *testing.T) { prov := newProv() escProvName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + provID := prov.GetID() type test struct { db acme.DB @@ -343,6 +405,7 @@ func TestHandler_NewAccount(t *testing.T) { b, err := json.Marshal(nar) assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, provisionerContextKey, prov) return test{ ctx: ctx, statusCode: 400, @@ -355,7 +418,8 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, statusCode: 500, @@ -368,7 +432,8 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) ctx = context.WithValue(ctx, jwkContextKey, nil) return test{ ctx: ctx, @@ -376,6 +441,27 @@ func TestHandler_NewAccount(t *testing.T) { err: acme.NewErrorISE("jwk expected in request context"), } }, + "fail/new-account-no-eab-provided": func(t *testing.T) test { + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: nil, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + return test{ + ctx: ctx, + statusCode: 400, + err: acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided"), + } + }, "fail/db.CreateAccount-error": func(t *testing.T) test { nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -385,6 +471,7 @@ func TestHandler_NewAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, provisionerContextKey, prov) ctx = context.WithValue(ctx, jwkContextKey, jwk) return test{ db: &acme.MockDB{ @@ -399,6 +486,109 @@ func TestHandler_NewAccount(t *testing.T) { err: acme.NewErrorISE("force"), } }, + "fail/acmeProvisionerFromContext": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + scepProvisioner := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + if err := scepProvisioner.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil { + assert.FatalError(t, err) + } + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + return test{ + ctx: ctx, + statusCode: 500, + err: acme.NewError(acme.ErrorServerInternalType, "provisioner in context is not an ACME provisioner"), + } + }, + "fail/db.UpdateExternalAccountKey-error": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + eak := &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + } + return test{ + db: &acme.MockDB{ + MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" + assert.Equals(t, acc.Contact, nar.Contact) + assert.Equals(t, acc.Key, jwk) + return nil + }, + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return eak, nil + }, + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { + return errors.New("force") + }, + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), + ExternalAccountBinding: eab, + }, + ctx: ctx, + statusCode: 500, + err: acme.NewError(acme.ErrorServerInternalType, "error updating external account binding key"), + } + }, "ok/new-account": func(t *testing.T) test { nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, @@ -455,6 +645,116 @@ func TestHandler_NewAccount(t *testing.T) { statusCode: 200, } }, + "ok/new-account-no-eab-required": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = false + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + return test{ + db: &acme.MockDB{ + MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" + assert.Equals(t, acc.Contact, nar.Contact) + assert.Equals(t, acc.Key, jwk) + return nil + }, + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), + }, + ctx: ctx, + statusCode: 201, + } + }, + "ok/new-account-with-eab": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" + assert.Equals(t, acc.Contact, nar.Contact) + assert.Equals(t, acc.Key, jwk) + return nil + }, + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + MockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { + return nil + }, + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + OrdersURL: fmt.Sprintf("%s/acme/%s/account/accountID/orders", baseURL.String(), escProvName), + ExternalAccountBinding: eab, + }, + ctx: ctx, + statusCode: 201, + } + }, } for name, run := range tests { tc := run(t) diff --git a/acme/api/eab.go b/acme/api/eab.go new file mode 100644 index 00000000..3660d066 --- /dev/null +++ b/acme/api/eab.go @@ -0,0 +1,155 @@ +package api + +import ( + "context" + "encoding/json" + + "github.com/smallstep/certificates/acme" + "go.step.sm/crypto/jose" +) + +// ExternalAccountBinding represents the ACME externalAccountBinding JWS +type ExternalAccountBinding struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` +} + +// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account. +func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { + acmeProv, err := acmeProvisionerFromContext(ctx) + if err != nil { + return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context") + } + + if !acmeProv.RequireEAB { + return nil, nil + } + + if nar.ExternalAccountBinding == nil { + return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided") + } + + eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) + if err != nil { + return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes") + } + + eabJWS, err := jose.ParseJWS(string(eabJSONBytes)) + if err != nil { + return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws") + } + + // TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration? + + keyID, acmeErr := validateEABJWS(ctx, eabJWS) + if acmeErr != nil { + return nil, acmeErr + } + + externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) + if err != nil { + if _, ok := err.(*acme.Error); ok { + return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") + } + return nil, acme.WrapErrorISE(err, "error retrieving external account key") + } + + if externalAccountKey.AlreadyBound() { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) + } + + payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) + if err != nil { + return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") + } + + jwk, err := jwkFromContext(ctx) + if err != nil { + return nil, err + } + + var payloadJWK *jose.JSONWebKey + if err = json.Unmarshal(payload, &payloadJWK); err != nil { + return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk") + } + + if !keysAreEqual(jwk, payloadJWK) { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match") + } + + return externalAccountKey, nil +} + +// keysAreEqual performs an equality check on two JWKs by comparing +// the (base64 encoding) of the Key IDs. +func keysAreEqual(x, y *jose.JSONWebKey) bool { + if x == nil || y == nil { + return false + } + digestX, errX := acme.KeyToID(x) + digestY, errY := acme.KeyToID(y) + if errX != nil || errY != nil { + return false + } + return digestX == digestY +} + +// validateEABJWS verifies the contents of the External Account Binding JWS. +// The protected header of the JWS MUST meet the following criteria: +// o The "alg" field MUST indicate a MAC-based algorithm +// o The "kid" field MUST contain the key identifier provided by the CA +// o The "nonce" field MUST NOT be present +// o The "url" field MUST be set to the same value as the outer JWS +func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) { + + if jws == nil { + return "", acme.NewErrorISE("no JWS provided") + } + + if len(jws.Signatures) != 1 { + return "", acme.NewError(acme.ErrorMalformedType, "JWS must have one signature") + } + + header := jws.Signatures[0].Protected + algorithm := header.Algorithm + keyID := header.KeyID + nonce := header.Nonce + + if !(algorithm == jose.HS256 || algorithm == jose.HS384 || algorithm == jose.HS512) { + return "", acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm '%s'", algorithm) + } + + if keyID == "" { + return "", acme.NewError(acme.ErrorMalformedType, "'kid' field is required") + } + + if nonce != "" { + return "", acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present") + } + + jwsURL, ok := header.ExtraHeaders["url"] + if !ok { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field is required") + } + + outerJWS, err := jwsFromContext(ctx) + if err != nil { + return "", acme.WrapErrorISE(err, "could not retrieve outer JWS from context") + } + + if len(outerJWS.Signatures) != 1 { + return "", acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature") + } + + outerJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders["url"] + if !ok { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS") + } + + if jwsURL != outerJWSURL { + return "", acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS") + } + + return keyID, nil +} diff --git a/acme/api/eab_test.go b/acme/api/eab_test.go new file mode 100644 index 00000000..dce9f36d --- /dev/null +++ b/acme/api/eab_test.go @@ -0,0 +1,1068 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/crypto/jose" +) + +func Test_keysAreEqual(t *testing.T) { + jwkX, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + jwkY, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + wrongJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + wrongJWK.Key = struct{}{} + type args struct { + x *jose.JSONWebKey + y *jose.JSONWebKey + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "ok/nil", + args: args{ + x: jwkX, + y: nil, + }, + want: false, + }, + { + name: "ok/equal", + args: args{ + x: jwkX, + y: jwkX, + }, + want: true, + }, + { + name: "ok/not-equal", + args: args{ + x: jwkX, + y: jwkY, + }, + want: false, + }, + { + name: "ok/wrong-key-type", + args: args{ + x: wrongJWK, + y: jwkY, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := keysAreEqual(tt.args.x, tt.args.y); got != tt.want { + t.Errorf("keysAreEqual() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHandler_validateExternalAccountBinding(t *testing.T) { + acmeProv := newACMEProv(t) + escProvName := url.PathEscape(acmeProv.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + provID := acmeProv.GetID() + type test struct { + db acme.DB + ctx context.Context + nar *NewAccountRequest + eak *acme.ExternalAccountKey + err *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok/no-eab-required-but-provided": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + prov := newACMEProv(t) + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + return test{ + db: &acme.MockDB{}, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: nil, + } + }, + "ok/eab": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + createdAt := time.Now() + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, + err: nil, + } + }, + "fail/acmeProvisionerFromContext": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + b, err := json.Marshal(nar) + assert.FatalError(t, err) + scepProvisioner := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + if err := scepProvisioner.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil { + assert.FatalError(t, err) + } + ctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b}) + ctx = context.WithValue(ctx, jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, scepProvisioner) + return test{ + ctx: ctx, + err: acme.NewError(acme.ErrorServerInternalType, "could not load ACME provisioner from context: provisioner in context is not an ACME provisioner"), + } + }, + "fail/parse-eab-jose": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + eab.Payload += "{}" + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + return test{ + db: &acme.MockDB{}, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error parsing externalAccountBinding jws"), + } + }, + "fail/validate-eab-jws-no-signatures": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + parsedJWS.Signatures = []jose.Signature{} + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{}, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature"), + } + }, + "fail/retrieve-eab-key-db-failure": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockError: errors.New("db failure"), + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error retrieving external account key"), + } + }, + "fail/db.GetExternalAccountKey-not-found": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return nil, acme.ErrNotFound + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error retrieving external account key"), + } + }, + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return nil, errors.New("force") + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error retrieving external account key"), + } + }, + "fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockError: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key: name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/eab-already-bound": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + createdAt := time.Now() + boundAt := time.Now().Add(1 * time.Second) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + CreatedAt: createdAt, + AccountID: "some-account-id", + BoundAt: boundAt, + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", "eakID", "some-account-id", boundAt), + } + }, + "fail/eab-verify": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 2, 3, 4}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewErrorISE("error verifying externalAccountBinding signature"), + } + }, + "fail/eab-non-matching-keys": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + differentJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(differentJWK, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, jwk) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match"), + } + }, + "fail/no-jwk": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorServerInternalType, "jwk expected in request context"), + } + }, + "fail/nil-jwk": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + rawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal(rawEABJWS, &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + parsedJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + prov := newACMEProv(t) + prov.RequireEAB = true + ctx := context.WithValue(context.Background(), jwkContextKey, nil) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, provisionerContextKey, prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + db: &acme.MockDB{ + MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { + return &acme.ExternalAccountKey{ + ID: "eakID", + ProvisionerID: provID, + Reference: "testeak", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: time.Now(), + }, nil + }, + }, + ctx: ctx, + nar: &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + }, + eak: nil, + err: acme.NewError(acme.ErrorServerInternalType, "jwk expected in request context"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + db: tc.db, + } + got, err := h.validateExternalAccountBinding(tc.ctx, tc.nar) + wantErr := tc.err != nil + gotErr := err != nil + if wantErr != gotErr { + t.Errorf("Handler.validateExternalAccountBinding() error = %v, want %v", err, tc.err) + } + if wantErr { + assert.NotNil(t, err) + assert.Type(t, &acme.Error{}, err) + ae, _ := err.(*acme.Error) + assert.Equals(t, ae.Type, tc.err.Type) + assert.Equals(t, ae.Status, tc.err.Status) + assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error()) + assert.Equals(t, ae.Detail, tc.err.Detail) + assert.Equals(t, ae.Identifier, tc.err.Identifier) + assert.Equals(t, ae.Subproblems, tc.err.Subproblems) + } else { + if got == nil { + assert.Nil(t, tc.eak) + } else { + assert.NotNil(t, tc.eak) + assert.Equals(t, got.ID, tc.eak.ID) + assert.Equals(t, got.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, got.Reference, tc.eak.Reference) + assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, got.AccountID, tc.eak.AccountID) + assert.Equals(t, got.BoundAt, tc.eak.BoundAt) + } + } + }) + } +} + +func Test_validateEABJWS(t *testing.T) { + acmeProv := newACMEProv(t) + escProvName := url.PathEscape(acmeProv.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + type test struct { + ctx context.Context + jws *jose.JSONWebSignature + keyID string + err *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/nil-jws": func(t *testing.T) test { + return test{ + jws: nil, + err: acme.NewErrorISE("no JWS provided"), + } + }, + "fail/invalid-number-of-signatures": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures = append(eabJWS.Signatures, jose.Signature{}) + return test{ + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "JWS must have one signature"), + } + }, + "fail/invalid-algorithm": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures[0].Protected.Algorithm = "HS42" + return test{ + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm 'HS42'"), + } + }, + "fail/kid-not-set": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures[0].Protected.KeyID = "" + return test{ + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "'kid' field is required"), + } + }, + "fail/nonce-not-empty": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + eabJWS.Signatures[0].Protected.Nonce = "some-bogus-nonce" + return test{ + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present"), + } + }, + "fail/url-not-set": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + delete(eabJWS.Signatures[0].Protected.ExtraHeaders, "url") + return test{ + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field is required"), + } + }, + "fail/no-outer-jws": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, nil) + return test{ + ctx: ctx, + jws: eabJWS, + err: acme.NewErrorISE("could not retrieve outer JWS from context"), + } + }, + "fail/outer-jws-multiple-signatures": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + outerJWS.Signatures = append(outerJWS.Signatures, jose.Signature{}) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature"), + } + }, + "fail/outer-jws-no-url": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS"), + } + }, + "fail/outer-jws-with-different-url": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", "this-is-not-the-same-url-as-in-the-eab-jws") + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: eabJWS, + err: acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS"), + } + }, + "ok": func(t *testing.T) test { + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + url := fmt.Sprintf("%s/acme/%s/account/new-account", baseURL.String(), escProvName) + eabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, "eakID", url) + assert.FatalError(t, err) + rawEABJWS := eabJWS.FullSerialize() + assert.FatalError(t, err) + eab := &ExternalAccountBinding{} + err = json.Unmarshal([]byte(rawEABJWS), &eab) + assert.FatalError(t, err) + nar := &NewAccountRequest{ + Contact: []string{"foo", "bar"}, + ExternalAccountBinding: eab, + } + payloadBytes, err := json.Marshal(nar) + assert.FatalError(t, err) + so := new(jose.SignerOptions) + so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm)) + so.WithHeader("url", url) + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + assert.FatalError(t, err) + jws, err := signer.Sign(payloadBytes) + assert.FatalError(t, err) + raw, err := jws.CompactSerialize() + assert.FatalError(t, err) + outerJWS, err := jose.ParseJWS(raw) + assert.FatalError(t, err) + ctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS) + return test{ + ctx: ctx, + jws: eabJWS, + keyID: "eakID", + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + keyID, err := validateEABJWS(tc.ctx, tc.jws) + wantErr := tc.err != nil + gotErr := err != nil + if wantErr != gotErr { + t.Errorf("validateEABJWS() error = %v, want %v", err, tc.err) + } + if wantErr { + assert.NotNil(t, err) + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Status, err.Status) + assert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error()) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, tc.err.Identifier, err.Identifier) + assert.Equals(t, tc.err.Subproblems, err.Subproblems) + } else { + assert.Nil(t, err) + assert.Equals(t, tc.keyID, keyID) + } + }) + } +} diff --git a/acme/api/handler.go b/acme/api/handler.go index 09ca03a3..bd226e73 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -136,6 +136,13 @@ func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) { } } +type Meta struct { + TermsOfService string `json:"termsOfService,omitempty"` + Website string `json:"website,omitempty"` + CaaIdentities []string `json:"caaIdentities,omitempty"` + ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"` +} + // Directory represents an ACME directory for configuring clients. type Directory struct { NewNonce string `json:"newNonce"` @@ -143,6 +150,7 @@ type Directory struct { NewOrder string `json:"newOrder"` RevokeCert string `json:"revokeCert"` KeyChange string `json:"keyChange"` + Meta Meta `json:"meta"` } // ToLog enables response logging for the Directory type. @@ -158,12 +166,21 @@ func (d *Directory) ToLog() (interface{}, error) { // for client configuration. func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + acmeProv, err := acmeProvisionerFromContext(ctx) + if err != nil { + api.WriteError(w, err) + return + } + api.JSON(w, &Directory{ NewNonce: h.linker.GetLink(ctx, NewNonceLinkType), NewAccount: h.linker.GetLink(ctx, NewAccountLinkType), NewOrder: h.linker.GetLink(ctx, NewOrderLinkType), RevokeCert: h.linker.GetLink(ctx, RevokeCertLinkType), KeyChange: h.linker.GetLink(ctx, KeyChangeLinkType), + Meta: Meta{ + ExternalAccountRequired: acmeProv.RequireEAB, + }, }) } diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index 14e00f12..67f7df30 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -15,9 +15,11 @@ import ( "time" "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" ) @@ -51,28 +53,76 @@ func TestHandler_GetNonce(t *testing.T) { func TestHandler_GetDirectory(t *testing.T) { linker := NewLinker("ca.smallstep.com", "acme") - - prov := newProv() - provName := url.PathEscape(prov.GetName()) - baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - - expDir := Directory{ - NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), - NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), - NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName), - RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName), - KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName), - } - type test struct { + ctx context.Context statusCode int + dir Directory err *acme.Error } var tests = map[string]func(t *testing.T) test{ + "fail/no-provisioner": func(t *testing.T) test { + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, nil) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + return test{ + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"), + } + }, + "fail/different-provisioner": func(t *testing.T) test { + prov := &provisioner.SCEP{ + Type: "SCEP", + Name: "test@scep-provisioner.com", + } + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + return test{ + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"), + } + }, "ok": func(t *testing.T) test { + prov := newProv() + provName := url.PathEscape(prov.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + expDir := Directory{ + NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), + NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), + NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName), + RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName), + KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName), + } return test{ + ctx: ctx, + dir: expDir, + statusCode: 200, + } + }, + "ok/eab-required": func(t *testing.T) test { + prov := newACMEProv(t) + prov.RequireEAB = true + provName := url.PathEscape(prov.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + expDir := Directory{ + NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName), + NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName), + NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName), + RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName), + KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName), + Meta: Meta{ + ExternalAccountRequired: true, + }, + } + return test{ + ctx: ctx, + dir: expDir, statusCode: 200, } }, @@ -82,7 +132,7 @@ func TestHandler_GetDirectory(t *testing.T) { t.Run(name, func(t *testing.T) { h := &Handler{linker: linker} req := httptest.NewRequest("GET", "/foo/bar", nil) - req = req.WithContext(ctx) + req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.GetDirectory(w, req) res := w.Result() @@ -105,7 +155,9 @@ func TestHandler_GetDirectory(t *testing.T) { } else { var dir Directory json.Unmarshal(bytes.TrimSpace(body), &dir) - assert.Equals(t, dir, expDir) + if !cmp.Equal(tc.dir, dir) { + t.Errorf("GetDirectory() diff =\n%s", cmp.Diff(tc.dir, dir)) + } assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) diff --git a/acme/api/linker.go b/acme/api/linker.go index d4490470..a605ffc3 100644 --- a/acme/api/linker.go +++ b/acme/api/linker.go @@ -3,13 +3,29 @@ package api import ( "context" "fmt" + "net" "net/url" + "strings" "github.com/smallstep/certificates/acme" ) // NewLinker returns a new Directory type. func NewLinker(dns, prefix string) Linker { + _, _, err := net.SplitHostPort(dns) + if err != nil && strings.Contains(err.Error(), "too many colons in address") { + // this is most probably an IPv6 without brackets, e.g. ::1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + // in case a port was appended to this wrong format, we try to extract the port, then check if it's + // still a valid IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443 (8443 is the port). If none of + // these cases, then the input dns is not changed. + lastIndex := strings.LastIndex(dns, ":") + hostPart, portPart := dns[:lastIndex], dns[lastIndex+1:] + if ip := net.ParseIP(hostPart); ip != nil { + dns = "[" + hostPart + "]:" + portPart + } else if ip := net.ParseIP(dns); ip != nil { + dns = "[" + dns + "]" + } + } return &linker{prefix: prefix, dns: dns} } diff --git a/acme/api/linker_test.go b/acme/api/linker_test.go index 4790dec8..74c2c8b0 100644 --- a/acme/api/linker_test.go +++ b/acme/api/linker_test.go @@ -31,6 +31,86 @@ func TestLinker_GetUnescapedPathSuffix(t *testing.T) { assert.Equals(t, getPath(CertificateLinkType, "{provisionerID}", "{certID}"), "/{provisionerID}/certificate/{certID}") } +func TestLinker_DNS(t *testing.T) { + prov := newProv() + escProvName := url.PathEscape(prov.GetName()) + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + type test struct { + name string + dns string + prefix string + expectedDirectoryLink string + } + tests := []test{ + { + name: "domain", + dns: "ca.smallstep.com", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://ca.smallstep.com/acme/%s/directory", escProvName), + }, + { + name: "domain-port", + dns: "ca.smallstep.com:8443", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://ca.smallstep.com:8443/acme/%s/directory", escProvName), + }, + { + name: "ipv4", + dns: "127.0.0.1", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://127.0.0.1/acme/%s/directory", escProvName), + }, + { + name: "ipv4-port", + dns: "127.0.0.1:8443", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://127.0.0.1:8443/acme/%s/directory", escProvName), + }, + { + name: "ipv6", + dns: "[::1]", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://[::1]/acme/%s/directory", escProvName), + }, + { + name: "ipv6-port", + dns: "[::1]:8443", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://[::1]:8443/acme/%s/directory", escProvName), + }, + { + name: "ipv6-no-brackets", + dns: "::1", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://[::1]/acme/%s/directory", escProvName), + }, + { + name: "ipv6-port-no-brackets", + dns: "::1:8443", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://[::1]:8443/acme/%s/directory", escProvName), + }, + { + name: "ipv6-long-no-brackets", + dns: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/acme/%s/directory", escProvName), + }, + { + name: "ipv6-long-port-no-brackets", + dns: "2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443", + prefix: "acme", + expectedDirectoryLink: fmt.Sprintf("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8443/acme/%s/directory", escProvName), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + linker := NewLinker(tt.dns, tt.prefix) + assert.Equals(t, tt.expectedDirectoryLink, linker.GetLink(ctx, DirectoryLinkType)) + }) + } +} + func TestLinker_GetLink(t *testing.T) { dns := "ca.smallstep.com" prefix := "acme" diff --git a/acme/api/middleware.go b/acme/api/middleware.go index d701f240..de8614ee 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -507,6 +507,20 @@ func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) { return pval, nil } +// acmeProvisionerFromContext searches the context for an ACME provisioner. Returns +// pointer to an ACME provisioner or an error. +func acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) { + prov, err := provisionerFromContext(ctx) + if err != nil { + return nil, err + } + acmeProv, ok := prov.(*provisioner.ACME) + if !ok || acmeProv == nil { + return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner") + } + return acmeProv, nil +} + // payloadFromContext searches the context for a payload. Returns the payload // or an error. func payloadFromContext(ctx context.Context) (*payloadInfo, error) { diff --git a/acme/db.go b/acme/db.go index 1675c7e7..412276fd 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,6 +19,13 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error + CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) + GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) + GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) + GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) + DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error + UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error + CreateNonce(ctx context.Context) (Nonce, error) DeleteNonce(ctx context.Context, nonce Nonce) error @@ -49,6 +56,13 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error + MockCreateExternalAccountKey func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) + MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) + MockGetExternalAccountKeys func(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) + MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) + MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error + MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error + MockCreateNonce func(ctx context.Context) (Nonce, error) MockDeleteNonce func(ctx context.Context, nonce Nonce) error @@ -114,6 +128,66 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error { return m.MockError } +// CreateExternalAccountKey mock +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) { + if m.MockCreateExternalAccountKey != nil { + return m.MockCreateExternalAccountKey(ctx, provisionerID, reference) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(*ExternalAccountKey), m.MockError +} + +// GetExternalAccountKey mock +func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) { + if m.MockGetExternalAccountKey != nil { + return m.MockGetExternalAccountKey(ctx, provisionerID, keyID) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(*ExternalAccountKey), m.MockError +} + +// GetExternalAccountKeys mock +func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) { + if m.MockGetExternalAccountKeys != nil { + return m.MockGetExternalAccountKeys(ctx, provisionerID, cursor, limit) + } else if m.MockError != nil { + return nil, "", m.MockError + } + return m.MockRet1.([]*ExternalAccountKey), "", m.MockError +} + +// GetExternalAccountKeyByReference mock +func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) { + if m.MockGetExternalAccountKeyByReference != nil { + return m.MockGetExternalAccountKeyByReference(ctx, provisionerID, reference) + } else if m.MockError != nil { + return nil, m.MockError + } + return m.MockRet1.(*ExternalAccountKey), m.MockError +} + +// DeleteExternalAccountKey mock +func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { + if m.MockDeleteExternalAccountKey != nil { + return m.MockDeleteExternalAccountKey(ctx, provisionerID, keyID) + } else if m.MockError != nil { + return m.MockError + } + return m.MockError +} + +// UpdateExternalAccountKey mock +func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error { + if m.MockUpdateExternalAccountKey != nil { + return m.MockUpdateExternalAccountKey(ctx, provisionerID, eak) + } else if m.MockError != nil { + return m.MockError + } + return m.MockError +} + // CreateNonce mock func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) { if m.MockCreateNonce != nil { diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index a02e93dc..83a23476 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -307,7 +307,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) { assert.Equals(t, string(key), accID) return nil, errors.New("force") default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, errors.New("force") } }, @@ -340,7 +340,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) { assert.Equals(t, string(key), accID) return b, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, errors.New("force") } }, @@ -462,7 +462,7 @@ func TestDB_CreateAccount(t *testing.T) { assert.True(t, dbacc.DeactivatedAt.IsZero()) return nil, false, errors.New("force") default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, false, errors.New("force") } }, @@ -506,7 +506,7 @@ func TestDB_CreateAccount(t *testing.T) { assert.True(t, dbacc.DeactivatedAt.IsZero()) return nu, true, nil default: - assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) return nil, false, errors.New("force") } }, diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go new file mode 100644 index 00000000..f9a24daf --- /dev/null +++ b/acme/db/nosql/eab.go @@ -0,0 +1,380 @@ +package nosql + +import ( + "context" + "crypto/rand" + "encoding/json" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" + nosqlDB "github.com/smallstep/nosql" +) + +// externalAccountKeyMutex for read/write locking of EAK operations. +var externalAccountKeyMutex sync.RWMutex + +// referencesByProvisionerIndexMutex for locking referencesByProvisioner index operations. +var referencesByProvisionerIndexMutex sync.Mutex + +type dbExternalAccountKey struct { + ID string `json:"id"` + ProvisionerID string `json:"provisionerID"` + Reference string `json:"reference"` + AccountID string `json:"accountID,omitempty"` + KeyBytes []byte `json:"key"` + CreatedAt time.Time `json:"createdAt"` + BoundAt time.Time `json:"boundAt"` +} + +type dbExternalAccountKeyReference struct { + Reference string `json:"reference"` + ExternalAccountKeyID string `json:"externalAccountKeyID"` +} + +// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey. +func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) { + data, err := db.db.Get(externalAccountKeyTable, []byte(id)) + if err != nil { + if nosqlDB.IsErrNotFound(err) { + return nil, acme.ErrNotFound + } + return nil, errors.Wrapf(err, "error loading external account key %s", id) + } + + dbeak := new(dbExternalAccountKey) + if err = json.Unmarshal(data, dbeak); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", id) + } + + return dbeak, nil +} + +// CreateExternalAccountKey creates a new External Account Binding key with a name +func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + + externalAccountKeyMutex.Lock() + defer externalAccountKeyMutex.Unlock() + + keyID, err := randID() + if err != nil { + return nil, err + } + + random := make([]byte, 32) + _, err = rand.Read(random) + if err != nil { + return nil, err + } + + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provisionerID, + Reference: reference, + KeyBytes: random, + CreatedAt: clock.Now(), + } + + if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil { + return nil, err + } + + if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil { + return nil, err + } + + if dbeak.Reference != "" { + dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ + Reference: dbeak.Reference, + ExternalAccountKeyID: dbeak.ID, + } + if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeyIDsByReferenceTable); err != nil { + return nil, err + } + } + + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} + +// GetExternalAccountKey retrieves an External Account Binding key by KeyID +func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) { + externalAccountKeyMutex.RLock() + defer externalAccountKeyMutex.RUnlock() + + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) + if err != nil { + return nil, err + } + + if dbeak.ProvisionerID != provisionerID { + return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created") + } + + return &acme.ExternalAccountKey{ + ID: dbeak.ID, + ProvisionerID: dbeak.ProvisionerID, + Reference: dbeak.Reference, + AccountID: dbeak.AccountID, + KeyBytes: dbeak.KeyBytes, + CreatedAt: dbeak.CreatedAt, + BoundAt: dbeak.BoundAt, + }, nil +} + +func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error { + externalAccountKeyMutex.Lock() + defer externalAccountKeyMutex.Unlock() + + dbeak, err := db.getDBExternalAccountKey(ctx, keyID) + if err != nil { + return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) + } + + if dbeak.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") + } + + if dbeak.Reference != "" { + if err := db.db.Del(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference) + } + } + if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { + return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) + } + if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil { + return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID) + } + + return nil +} + +// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + externalAccountKeyMutex.RLock() + defer externalAccountKeyMutex.RUnlock() + + // cursor and limit are ignored in open source, at least for now. + + var eakIDs []string + r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return nil, "", errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(r, &eakIDs); err != nil { + return nil, "", errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) + } + } + + keys := []*acme.ExternalAccountKey{} + for _, eakID := range eakIDs { + if eakID == "" { + continue // shouldn't happen; just in case + } + eak, err := db.getDBExternalAccountKey(ctx, eakID) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return nil, "", errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID) + } + } + keys = append(keys, &acme.ExternalAccountKey{ + ID: eak.ID, + KeyBytes: eak.KeyBytes, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + }) + } + + return keys, "", nil +} + +// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference +func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { + externalAccountKeyMutex.RLock() + defer externalAccountKeyMutex.RUnlock() + + if reference == "" { + return nil, nil + } + + k, err := db.db.Get(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, reference))) + if nosqlDB.IsErrNotFound(err) { + return nil, acme.ErrNotFound + } else if err != nil { + return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference) + } + dbExternalAccountKeyReference := new(dbExternalAccountKeyReference) + if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) + } + + return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) +} + +func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { + externalAccountKeyMutex.Lock() + defer externalAccountKeyMutex.Unlock() + + old, err := db.getDBExternalAccountKey(ctx, eak.ID) + if err != nil { + return err + } + + if old.ProvisionerID != provisionerID { + return errors.New("provisioner does not match provisioner for which the EAB key was created") + } + + if old.ProvisionerID != eak.ProvisionerID { + return errors.New("cannot change provisioner for an existing ACME EAB Key") + } + + if old.Reference != eak.Reference { + return errors.New("cannot change reference for an existing ACME EAB Key") + } + + nu := dbExternalAccountKey{ + ID: eak.ID, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + KeyBytes: eak.KeyBytes, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + } + + return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) +} + +func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMutex.Lock() + defer referencesByProvisionerIndexMutex.Unlock() + + if eakID == "" { + return errors.Errorf("can't add empty eakID for provisioner %s", provisionerID) + } + + var eakIDs []string + b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + for _, id := range eakIDs { + if id == eakID { + // return an error when a duplicate ID is found + return errors.Errorf("eakID %s already exists for provisioner %s", eakID, provisionerID) + } + } + + var newEAKIDs []string + newEAKIDs = append(newEAKIDs, eakIDs...) + newEAKIDs = append(newEAKIDs, eakID) + + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + // ensure that the DB gets the expected value when the slice is empty; otherwise + // it'll return with an error that indicates that the DBs view of the data is + // different from the last read (i.e. _old is different from what the DB has). + if len(eakIDs) == 0 { + _old = nil + } + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + + return nil +} + +func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error { + referencesByProvisionerIndexMutex.Lock() + defer referencesByProvisionerIndexMutex.Unlock() + + var eakIDs []string + b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(b, &eakIDs); err != nil { + return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID) + } + } + + newEAKIDs := removeElement(eakIDs, eakID) + var ( + _old interface{} = eakIDs + _new interface{} = newEAKIDs + ) + + // ensure that the DB gets the expected value when the slice is empty; otherwise + // it'll return with an error that indicates that the DBs view of the data is + // different from the last read (i.e. _old is different from what the DB has). + if len(eakIDs) == 0 { + _old = nil + } + + if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + + return nil +} + +// referenceKey returns a unique key for a reference per provisioner +func referenceKey(provisionerID, reference string) string { + return provisionerID + "." + reference +} + +// sliceIndex finds the index of item in slice +func sliceIndex(slice []string, item string) int { + for i := range slice { + if slice[i] == item { + return i + } + } + return -1 +} + +// removeElement deletes the item if it exists in the +// slice. It returns a new slice, keeping the old one intact. +func removeElement(slice []string, item string) []string { + + newSlice := make([]string, 0) + index := sliceIndex(slice, item) + if index < 0 { + newSlice = append(newSlice, slice...) + return newSlice + } + + newSlice = append(newSlice, slice[:index]...) + + return append(newSlice, slice[index+1:]...) +} diff --git a/acme/db/nosql/eab_test.go b/acme/db/nosql/eab_test.go new file mode 100644 index 00000000..568500e9 --- /dev/null +++ b/acme/db/nosql/eab_test.go @@ -0,0 +1,1712 @@ +package nosql + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" + certdb "github.com/smallstep/certificates/db" + "github.com/smallstep/nosql" + nosqldb "github.com/smallstep/nosql/database" +) + +func TestDB_getDBExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + dbeak *dbExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: nil, + dbeak: dbeak, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: acme.ErrNotFound, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return []byte("foo"), nil + }, + }, + err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.dbeak.ID) + assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) + assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID) + assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) + assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) + assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) + assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) + } + }) + } +} + +func TestDB_GetExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, eak.Reference, tc.eak.Reference) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + } + }) + } +} + +func TestDB_GetExternalAccountKeyByReference(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + ref string + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + err: nil, + } + }, + "ok/no-reference": func(t *testing.T) test { + return test{ + ref: "", + eak: nil, + err: nil, + } + }, + "fail/reference-not-found": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("not found"), + } + }, + "fail/reference-load-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB key for reference ref: force"), + } + }, + "fail/reference-unmarshal-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable)) + assert.Equals(t, string(key), provID+"."+ref) + return []byte{0}, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB key for reference ref"), + } + }, + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else if assert.Nil(t, tc.err) && tc.eak != nil { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, eak.Reference, tc.eak.Reference) + } + }) + } +} + +func TestDB_GetExternalAccountKeys(t *testing.T) { + keyID1 := "keyID1" + keyID2 := "keyID2" + keyID3 := "keyID3" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak1 := &dbExternalAccountKey{ + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b1, err := json.Marshal(dbeak1) + assert.FatalError(t, err) + dbeak2 := &dbExternalAccountKey{ + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b2, err := json.Marshal(dbeak2) + assert.FatalError(t, err) + dbeak3 := &dbExternalAccountKey{ + ID: keyID3, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b3, err := json.Marshal(dbeak3) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{"", keyID1, keyID2} // includes an empty keyID + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + switch string(key) { + case keyID1: + return b1, nil + case keyID2: + return b2, nil + default: + assert.FatalError(t, errors.Errorf("unexpected key %s", string(key))) + return nil, errors.New("force default") + } + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + // TODO: remove the MList + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + switch string(bucket) { + case string(externalAccountKeyTable): + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: b1, + }, + { + Bucket: bucket, + Key: []byte(keyID2), + Value: b2, + }, + { + Bucket: bucket, + Key: []byte(keyID3), + Value: b3, + }, + }, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(provID), + Value: b, + }, + }, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + }, + eaks: []*acme.ExternalAccountKey{ + { + ID: keyID1, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + { + ID: keyID2, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + }, + } + }, + "fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"), + } + }, + "fail/db.Get-externalAccountKeysByProvisionerIDTable-unmarshal": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB Key IDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/db.getDBExternalAccountKey": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + keys := []string{keyID1, keyID2} + b, err := json.Marshal(keys) + assert.FatalError(t, err) + return b, nil + case string(externalAccountKeyTable): + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force bucket") + } + }, + }, + err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + cursor, limit := "", 0 + if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil { + assert.Equals(t, "", nextCursor) + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.Equals(t, tc.err.Error(), err.Error()) + } + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, len(eaks), len(tc.eaks)) + assert.Equals(t, "", nextCursor) + for i, eak := range eaks { + assert.Equals(t, eak.ID, tc.eaks[i].ID) + assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) + assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID) + assert.Equals(t, eak.Reference, tc.eaks[i].Reference) + assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) + assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) + assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) + } + } + }) + } +} + +func TestDB_DeleteExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + b, err := json.Marshal([]string{keyID}) + assert.FatalError(t, err) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) { + fmt.Println(string(bucket)) + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + return nil, true, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nil, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/delete-reference": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"), + } + }, + "fail/delete-eak": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"), + } + }, + "fail/delete-eakID": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + case string(externalAccountKeyIDsByProvisionerIDTable): + return b, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force default") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), provID+"."+ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return errors.New("force default") + } + }, + }, + err: errors.New("error removing ACME EAB Key ID keyID: error loading eakIDs for provisioner provID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.Equals(t, err.Error(), tc.err.Error()) + } + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestDB_CreateExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + type test struct { + db nosql.DB + err error + _id *string + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + var ( + id string + idPtr = &id + ) + now := clock.Now() + eak := &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "ref", + AccountID: "", + CreatedAt: now, + } + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + assert.Equals(t, nil, old) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, nil, old) + + id = string(key) + + dbeak := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbeak)) + assert.Equals(t, string(key), dbeak.ID) + assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID) + assert.Equals(t, eak.Reference, dbeak.Reference) + assert.Equals(t, 32, len(dbeak.KeyBytes)) + assert.False(t, dbeak.CreatedAt.IsZero()) + assert.Equals(t, dbeak.AccountID, eak.AccountID) + assert.True(t, dbeak.BoundAt.IsZero()) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + eak: eak, + _id: idPtr, + } + }, + "fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error saving acme external_account_key: force"), + } + }, + "fail/addEAKID-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + return nil, errors.New("force") + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable)) + assert.Equals(t, provID, string(key)) + b, _ := json.Marshal([]string{}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(externalAccountKeyIDsByProvisionerIDTable): + assert.Equals(t, provID, string(key)) + return nu, true, nil + case string(externalAccountKeyIDsByReferenceTable): + assert.Equals(t, provID+"."+ref, string(key)) + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force default") + } + }, + }, + err: errors.New("error saving acme external_account_key_reference: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.Equals(t, err.Error(), tc.err.Error()) + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, *tc._id, eak.ID) + assert.Equals(t, provID, eak.ProvisionerID) + assert.Equals(t, ref, eak.Reference) + assert.Equals(t, "", eak.AccountID) + assert.False(t, eak.CreatedAt.IsZero()) + assert.False(t, eak.AlreadyBound()) + assert.True(t, eak.BoundAt.IsZero()) + } + }) + } +} + +func TestDB_UpdateExternalAccountKey(t *testing.T) { + keyID := "keyID" + provID := "provID" + ref := "ref" + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + type test struct { + db nosql.DB + eak *acme.ExternalAccountKey + err error + } + var tests = map[string]func(t *testing.T) test{ + + "ok": func(t *testing.T) test { + eak := &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + return test{ + eak: eak, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, old, b) + + dbNew := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbNew)) + assert.Equals(t, dbNew.ID, dbeak.ID) + assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID) + assert.Equals(t, dbNew.Reference, dbeak.Reference) + assert.Equals(t, dbNew.AccountID, dbeak.AccountID) + assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) + assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) + assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) + return nu, true, nil + }, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/provisioner-mismatch": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvID", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + }, + err: errors.New("provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/provisioner-change": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: "aDifferentProvisionerID", + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("cannot change provisioner for an existing ACME EAB Key"), + } + }, + "fail/reference-change": func(t *testing.T) test { + newDBEAK := &dbExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(newDBEAK) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + ProvisionerID: provID, + Reference: "aDifferentReference", + }, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("cannot change reference for an existing ACME EAB Key"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + d := DB{db: tc.db} + if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.eak.ID) + assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID) + assert.Equals(t, dbeak.Reference, tc.eak.Reference) + assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) + assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) + } + }) + } +} + +func TestDB_addEAKID(t *testing.T) { + provID := "provID" + eakID := "eakID" + type test struct { + ctx context.Context + provisionerID string + eakID string + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "fail/empty-eakID": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: "", + err: errors.New("can't add empty eakID for provisioner provID"), + } + }, + "fail/db.Get": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/unmarshal": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/eakID-already-exists": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{eakID}) + return b, nil + }, + }, + err: errors.New("eakID eakID already exists for provisioner provID"), + } + }, + "fail/db.save": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", eakID}) + assert.Equals(t, nu, newB) + return newB, true, errors.New("force") + }, + }, + err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"), + } + }, + "ok/db.Get-not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, nosqldb.ErrNotFound + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + assert.Equals(t, old, nil) + b, _ := json.Marshal([]string{eakID}) + assert.Equals(t, nu, b) + return b, true, nil + }, + }, + err: nil, + } + }, + "ok": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", "id2"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", "id2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", "id2", eakID}) + assert.Equals(t, nu, newB) + return newB, true, nil + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := &DB{ + db: tc.db, + } + wantErr := tc.err != nil + err := db.addEAKID(tc.ctx, tc.provisionerID, tc.eakID) + if (err != nil) != wantErr { + t.Errorf("DB.addEAKID() error = %v, wantErr %v", err, wantErr) + } + if err != nil { + assert.Equals(t, tc.err.Error(), err.Error()) + } + }) + } +} + +func TestDB_deleteEAKID(t *testing.T) { + provID := "provID" + eakID := "eakID" + type test struct { + ctx context.Context + provisionerID string + eakID string + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "fail/db.Get": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading eakIDs for provisioner provID: force"), + } + }, + "fail/unmarshal": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal(1) + return b, nil + }, + }, + err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"), + } + }, + "fail/db.save": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", eakID}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", eakID}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1"}) + assert.Equals(t, nu, newB) + return newB, true, errors.New("force") + }, + }, + err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"), + } + }, + "ok/db.Get-not-found": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + return nil, nosqldb.ErrNotFound + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + assert.Equals(t, old, nil) + b, _ := json.Marshal([]string{}) + assert.Equals(t, nu, b) + return b, true, nil + }, + }, + err: nil, + } + }, + "ok": func(t *testing.T) test { + return test{ + ctx: context.Background(), + provisionerID: provID, + eakID: eakID, + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + b, _ := json.Marshal([]string{"id1", eakID, "id2"}) + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + oldB, _ := json.Marshal([]string{"id1", eakID, "id2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"id1", "id2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := &DB{ + db: tc.db, + } + wantErr := tc.err != nil + err := db.deleteEAKID(tc.ctx, tc.provisionerID, tc.eakID) + if (err != nil) != wantErr { + t.Errorf("DB.deleteEAKID() error = %v, wantErr %v", err, wantErr) + } + if err != nil { + assert.Equals(t, tc.err.Error(), err.Error()) + } + }) + } +} + +func TestDB_addAndDeleteEAKID(t *testing.T) { + provID := "provID" + callCounter := 0 + type test struct { + ctx context.Context + db nosql.DB + err error + } + var tests = map[string]func(t *testing.T) test{ + "ok/multi": func(t *testing.T) test { + return test{ + ctx: context.Background(), + db: &certdb.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + switch callCounter { + case 0: + return nil, nosqldb.ErrNotFound + case 1: + b, _ := json.Marshal([]string{"eakID"}) + return b, nil + case 2: + b, _ := json.Marshal([]string{}) + return b, nil + case 3: + b, _ := json.Marshal([]string{"eakID1"}) + return b, nil + case 4: + b, _ := json.Marshal([]string{"eakID1", "eakID2"}) + return b, nil + case 5: + b, _ := json.Marshal([]string{"eakID2"}) + return b, nil + default: + assert.FatalError(t, errors.New("unexpected get iteration")) + return nil, errors.New("force get default") + } + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable) + assert.Equals(t, string(key), provID) + switch callCounter { + case 0: + assert.Equals(t, old, nil) + newB, _ := json.Marshal([]string{"eakID"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 1: + oldB, _ := json.Marshal([]string{"eakID"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{}) + return newB, true, nil + case 2: + assert.Equals(t, old, nil) + newB, _ := json.Marshal([]string{"eakID1"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 3: + oldB, _ := json.Marshal([]string{"eakID1"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID1", "eakID2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 4: + oldB, _ := json.Marshal([]string{"eakID1", "eakID2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{"eakID2"}) + assert.Equals(t, nu, newB) + return newB, true, nil + case 5: + oldB, _ := json.Marshal([]string{"eakID2"}) + assert.Equals(t, old, oldB) + newB, _ := json.Marshal([]string{}) + assert.Equals(t, nu, newB) + return newB, true, nil + default: + assert.FatalError(t, errors.New("unexpected get iteration")) + return nil, true, errors.New("force save default") + } + }, + }, + err: nil, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + + // goal of this test is to simulate multiple calls; no errors expected. + + db := &DB{ + db: tc.db, + } + + err := db.addEAKID(tc.ctx, provID, "eakID") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID") + if err != nil { + t.Errorf("DB.deleteEAKID() error = %v", err) + } + + callCounter++ + err = db.addEAKID(tc.ctx, provID, "eakID1") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.addEAKID(tc.ctx, provID, "eakID2") + if err != nil { + t.Errorf("DB.addEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID1") + if err != nil { + t.Errorf("DB.deleteEAKID() error = %v", err) + } + + callCounter++ + err = db.deleteEAKID(tc.ctx, provID, "eakID2") + if err != nil { + t.Errorf("DB.deleteAKID() error = %v", err) + } + }) + } +} + +func Test_removeElement(t *testing.T) { + tests := []struct { + name string + slice []string + item string + want []string + }{ + { + name: "remove-first", + slice: []string{"id1", "id2", "id3"}, + item: "id1", + want: []string{"id2", "id3"}, + }, + { + name: "remove-last", + slice: []string{"id1", "id2", "id3"}, + item: "id3", + want: []string{"id1", "id2"}, + }, + { + name: "remove-middle", + slice: []string{"id1", "id2", "id3"}, + item: "id2", + want: []string{"id1", "id3"}, + }, + { + name: "remove-non-existing", + slice: []string{"id1", "id2", "id3"}, + item: "none", + want: []string{"id1", "id2", "id3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := removeElement(tt.slice, tt.item) + if !cmp.Equal(tt.want, got) { + t.Errorf("removeElement() diff =\n %s", cmp.Diff(tt.want, got)) + } + }) + } +} diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index 34932361..98f6a04d 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -11,15 +11,18 @@ import ( ) 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") - certBySerialTable = []byte("acme_serial_certs_index") + 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") + certBySerialTable = []byte("acme_serial_certs_index") + externalAccountKeyTable = []byte("acme_external_account_keys") + externalAccountKeyIDsByReferenceTable = []byte("acme_external_account_keyID_reference_index") + externalAccountKeyIDsByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index") ) // DB is a struct that implements the AcmeDB interface. @@ -30,7 +33,10 @@ type DB struct { // New configures and returns a new ACME DB backend implemented using a nosql DB. func New(db nosqlDB.DB) (*DB, error) { tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, - challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable, certBySerialTable} + challengeTable, nonceTable, orderTable, ordersByAccountIDTable, + certTable, certBySerialTable, externalAccountKeyTable, + externalAccountKeyIDsByReferenceTable, externalAccountKeyIDsByProvisionerIDTable, + } for _, b := range tables { if err := db.CreateTable(b); err != nil { return nil, errors.Wrapf(err, "error creating table %s", diff --git a/api/api_test.go b/api/api_test.go index 5cbce8b3..c7528f9b 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -167,6 +167,208 @@ func parseCertificateRequest(data string) *x509.CertificateRequest { return csr } +type mockAuthority struct { + ret1, ret2 interface{} + err error + authorizeSign func(ott string) ([]provisioner.SignOption, error) + getTLSOptions func() *authority.TLSOptions + root func(shasum string) (*x509.Certificate, error) + sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) + renew func(cert *x509.Certificate) ([]*x509.Certificate, error) + rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) + loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) + loadProvisionerByName func(name string) (provisioner.Interface, error) + getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) + revoke func(context.Context, *authority.RevokeOptions) error + getEncryptedKey func(kid string) (string, error) + getRoots func() ([]*x509.Certificate, error) + getFederation func() ([]*x509.Certificate, error) + signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) + renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) + rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) + getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) + getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error) + getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error) + getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) + checkSSHHost func(ctx context.Context, principal, token string) (bool, error) + getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error) + version func() authority.Version +} + +// TODO: remove once Authorize is deprecated. +func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { + return m.AuthorizeSign(ott) +} + +func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { + if m.authorizeSign != nil { + return m.authorizeSign(ott) + } + return m.ret1.([]provisioner.SignOption), m.err +} + +func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions { + if m.getTLSOptions != nil { + return m.getTLSOptions() + } + return m.ret1.(*authority.TLSOptions) +} + +func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) { + if m.root != nil { + return m.root(shasum) + } + return m.ret1.(*x509.Certificate), m.err +} + +func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + if m.sign != nil { + return m.sign(cr, opts, signOpts...) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { + if m.renew != nil { + return m.renew(cert) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { + if m.rekey != nil { + return m.rekey(oldcert, pk) + } + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err +} + +func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { + if m.getProvisioners != nil { + return m.getProvisioners(nextCursor, limit) + } + return m.ret1.(provisioner.List), m.ret2.(string), m.err +} + +func (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) { + if m.loadProvisionerByCertificate != nil { + return m.loadProvisionerByCertificate(cert) + } + return m.ret1.(provisioner.Interface), m.err +} + +func (m *mockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { + if m.loadProvisionerByName != nil { + return m.loadProvisionerByName(name) + } + return m.ret1.(provisioner.Interface), m.err +} + +func (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { + if m.revoke != nil { + return m.revoke(ctx, opts) + } + return m.err +} + +func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) { + if m.getEncryptedKey != nil { + return m.getEncryptedKey(kid) + } + return m.ret1.(string), m.err +} + +func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) { + if m.getRoots != nil { + return m.getRoots() + } + return m.ret1.([]*x509.Certificate), m.err +} + +func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) { + if m.getFederation != nil { + return m.getFederation() + } + return m.ret1.([]*x509.Certificate), m.err +} + +func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + if m.signSSH != nil { + return m.signSSH(ctx, key, opts, signOpts...) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) { + if m.signSSHAddUser != nil { + return m.signSSHAddUser(ctx, key, cert) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) { + if m.renewSSH != nil { + return m.renewSSH(ctx, cert) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + if m.rekeySSH != nil { + return m.rekeySSH(ctx, cert, key, signOpts...) + } + return m.ret1.(*ssh.Certificate), m.err +} + +func (m *mockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) { + if m.getSSHHosts != nil { + return m.getSSHHosts(ctx, cert) + } + return m.ret1.([]authority.Host), m.err +} + +func (m *mockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) { + if m.getSSHRoots != nil { + return m.getSSHRoots(ctx) + } + return m.ret1.(*authority.SSHKeys), m.err +} + +func (m *mockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) { + if m.getSSHFederation != nil { + return m.getSSHFederation(ctx) + } + return m.ret1.(*authority.SSHKeys), m.err +} + +func (m *mockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { + if m.getSSHConfig != nil { + return m.getSSHConfig(ctx, typ, data) + } + return m.ret1.([]templates.Output), m.err +} + +func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { + if m.checkSSHHost != nil { + return m.checkSSHHost(ctx, principal, token) + } + return m.ret1.(bool), m.err +} + +func (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) { + if m.getSSHBastion != nil { + return m.getSSHBastion(ctx, user, hostname) + } + return m.ret1.(*authority.Bastion), m.err +} + +func (m *mockAuthority) Version() authority.Version { + if m.version != nil { + return m.version() + } + return m.ret1.(authority.Version) +} + func TestNewCertificate(t *testing.T) { cert := parseCertificate(rootPEM) if !reflect.DeepEqual(Certificate{Certificate: cert}, NewCertificate(cert)) { @@ -551,208 +753,6 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) ( return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err } -type mockAuthority struct { - ret1, ret2 interface{} - err error - authorizeSign func(ott string) ([]provisioner.SignOption, error) - getTLSOptions func() *authority.TLSOptions - root func(shasum string) (*x509.Certificate, error) - sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) - renew func(cert *x509.Certificate) ([]*x509.Certificate, error) - rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) - loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) - loadProvisionerByName func(name string) (provisioner.Interface, error) - getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) - revoke func(context.Context, *authority.RevokeOptions) error - getEncryptedKey func(kid string) (string, error) - getRoots func() ([]*x509.Certificate, error) - getFederation func() ([]*x509.Certificate, error) - signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) - signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) - renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) - rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) - getSSHHosts func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) - getSSHRoots func(ctx context.Context) (*authority.SSHKeys, error) - getSSHFederation func(ctx context.Context) (*authority.SSHKeys, error) - getSSHConfig func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) - checkSSHHost func(ctx context.Context, principal, token string) (bool, error) - getSSHBastion func(ctx context.Context, user string, hostname string) (*authority.Bastion, error) - version func() authority.Version -} - -// TODO: remove once Authorize is deprecated. -func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { - return m.AuthorizeSign(ott) -} - -func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) { - if m.authorizeSign != nil { - return m.authorizeSign(ott) - } - return m.ret1.([]provisioner.SignOption), m.err -} - -func (m *mockAuthority) GetTLSOptions() *authority.TLSOptions { - if m.getTLSOptions != nil { - return m.getTLSOptions() - } - return m.ret1.(*authority.TLSOptions) -} - -func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) { - if m.root != nil { - return m.root(shasum) - } - return m.ret1.(*x509.Certificate), m.err -} - -func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { - if m.sign != nil { - return m.sign(cr, opts, signOpts...) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { - if m.renew != nil { - return m.renew(cert) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { - if m.rekey != nil { - return m.rekey(oldcert, pk) - } - return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err -} - -func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { - if m.getProvisioners != nil { - return m.getProvisioners(nextCursor, limit) - } - return m.ret1.(provisioner.List), m.ret2.(string), m.err -} - -func (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) { - if m.loadProvisionerByCertificate != nil { - return m.loadProvisionerByCertificate(cert) - } - return m.ret1.(provisioner.Interface), m.err -} - -func (m *mockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { - if m.loadProvisionerByName != nil { - return m.loadProvisionerByName(name) - } - return m.ret1.(provisioner.Interface), m.err -} - -func (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error { - if m.revoke != nil { - return m.revoke(ctx, opts) - } - return m.err -} - -func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) { - if m.getEncryptedKey != nil { - return m.getEncryptedKey(kid) - } - return m.ret1.(string), m.err -} - -func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) { - if m.getRoots != nil { - return m.getRoots() - } - return m.ret1.([]*x509.Certificate), m.err -} - -func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) { - if m.getFederation != nil { - return m.getFederation() - } - return m.ret1.([]*x509.Certificate), m.err -} - -func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - if m.signSSH != nil { - return m.signSSH(ctx, key, opts, signOpts...) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) { - if m.signSSHAddUser != nil { - return m.signSSHAddUser(ctx, key, cert) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) { - if m.renewSSH != nil { - return m.renewSSH(ctx, cert) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - if m.rekeySSH != nil { - return m.rekeySSH(ctx, cert, key, signOpts...) - } - return m.ret1.(*ssh.Certificate), m.err -} - -func (m *mockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) { - if m.getSSHHosts != nil { - return m.getSSHHosts(ctx, cert) - } - return m.ret1.([]authority.Host), m.err -} - -func (m *mockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) { - if m.getSSHRoots != nil { - return m.getSSHRoots(ctx) - } - return m.ret1.(*authority.SSHKeys), m.err -} - -func (m *mockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) { - if m.getSSHFederation != nil { - return m.getSSHFederation(ctx) - } - return m.ret1.(*authority.SSHKeys), m.err -} - -func (m *mockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) { - if m.getSSHConfig != nil { - return m.getSSHConfig(ctx, typ, data) - } - return m.ret1.([]templates.Output), m.err -} - -func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { - if m.checkSSHHost != nil { - return m.checkSSHHost(ctx, principal, token) - } - return m.ret1.(bool), m.err -} - -func (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) { - if m.getSSHBastion != nil { - return m.getSSHBastion(ctx, user, hostname) - } - return m.ret1.(*authority.Bastion), m.err -} - -func (m *mockAuthority) Version() authority.Version { - if m.version != nil { - return m.version() - } - return m.ret1.(authority.Version) -} - func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go new file mode 100644 index 00000000..27c3ba6f --- /dev/null +++ b/authority/admin/api/acme.go @@ -0,0 +1,115 @@ +package api + +import ( + "context" + "fmt" + "net/http" + + "github.com/go-chi/chi" + "github.com/smallstep/certificates/api" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" +) + +const ( + // provisionerContextKey provisioner key + provisionerContextKey = ContextKey("provisioner") +) + +// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests +type CreateExternalAccountKeyRequest struct { + Reference string `json:"reference"` +} + +// Validate validates a new ACME EAB Key request body. +func (r *CreateExternalAccountKeyRequest) Validate() error { + if len(r.Reference) > 256 { // an arbitrary, but sensible (IMO), limit + return fmt.Errorf("reference length %d exceeds the maximum (256)", len(r.Reference)) + } + return nil +} + +// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses +type GetExternalAccountKeysResponse struct { + EAKs []*linkedca.EABKey `json:"eaks"` + NextCursor string `json:"nextCursor"` +} + +// requireEABEnabled is a middleware that ensures ACME EAB is enabled +// before serving requests that act on ACME EAB credentials. +func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + provName := chi.URLParam(r, "provisionerName") + eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName) + if err != nil { + api.WriteError(w, err) + return + } + if !eabEnabled { + api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov.GetName())) + return + } + ctx = context.WithValue(ctx, provisionerContextKey, prov) + next(w, r.WithContext(ctx)) + } +} + +// provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME +// provisioner is set to true and thus has EAB enabled. +func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *linkedca.Provisioner, error) { + var ( + p provisioner.Interface + err error + ) + if p, err = h.auth.LoadProvisionerByName(provisionerName); err != nil { + return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName) + } + + prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) + if err != nil { + return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID()) + } + + details := prov.GetDetails() + if details == nil { + return false, nil, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID()) + } + + acmeProvisioner := details.GetACME() + if acmeProvisioner == nil { + return false, nil, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) + } + + return acmeProvisioner.GetRequireEab(), prov, nil +} + +type acmeAdminResponderInterface interface { + GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) + CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) + DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) +} + +// ACMEAdminResponder is responsible for writing ACME admin responses +type ACMEAdminResponder struct{} + +// NewACMEAdminResponder returns a new ACMEAdminResponder +func NewACMEAdminResponder() *ACMEAdminResponder { + return &ACMEAdminResponder{} +} + +// GetExternalAccountKeys writes the response for the EAB keys GET endpoint +func (h *ACMEAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { + api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm")) +} + +// CreateExternalAccountKey writes the response for the EAB key POST endpoint +func (h *ACMEAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) { + api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm")) +} + +// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint +func (h *ACMEAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { + api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm")) +} diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go new file mode 100644 index 00000000..6ffe1418 --- /dev/null +++ b/authority/admin/api/acme_test.go @@ -0,0 +1,587 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +func readProtoJSON(r io.ReadCloser, m proto.Message) error { + defer r.Close() + data, err := io.ReadAll(r) + if err != nil { + return err + } + return protojson.Unmarshal(data, m) +} + +func TestHandler_requireEABEnabled(t *testing.T) { + type test struct { + ctx context.Context + adminDB admin.DB + auth adminAuthority + next nextHTTP + err *admin.Error + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/h.provisionerHasEABEnabled": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + err := admin.NewErrorISE("error loading provisioner provName: force") + err.Message = "error loading provisioner provName: force" + return test{ + ctx: ctx, + auth: auth, + err: err, + statusCode: 500, + } + }, + "ok/eab-disabled": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: false, + }, + }, + }, + }, nil + }, + } + err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName") + err.Message = "ACME EAB not enabled for provisioner provName" + return test{ + ctx: ctx, + auth: auth, + adminDB: db, + err: err, + statusCode: 400, + } + }, + "ok/eab-enabled": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: true, + }, + }, + }, + }, nil + }, + } + return test{ + ctx: ctx, + auth: auth, + adminDB: db, + next: func(w http.ResponseWriter, r *http.Request) { + w.Write(nil) // mock response with status 200 + }, + statusCode: 200, + } + }, + } + + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: nil, + } + + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.requireEABEnabled(tc.next)(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + err := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err)) + + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Message, err.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + }) + } +} + +func TestHandler_provisionerHasEABEnabled(t *testing.T) { + type test struct { + adminDB admin.DB + auth adminAuthority + provisionerName string + want bool + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + return test{ + auth: auth, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "fail/db.GetProvisioner": func(t *testing.T) test { + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + auth: auth, + adminDB: db, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "fail/prov.GetDetails": func(t *testing.T) test { + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: nil, + }, nil + }, + } + return test{ + auth: auth, + adminDB: db, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "fail/details.GetACME": func(t *testing.T) test { + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: nil, + }, + }, + }, nil + }, + } + return test{ + auth: auth, + adminDB: db, + provisionerName: "provName", + want: false, + err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"), + } + }, + "ok/eab-disabled": func(t *testing.T) test { + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "eab-disabled", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "eab-disabled", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: false, + }, + }, + }, + }, nil + }, + } + return test{ + adminDB: db, + auth: auth, + provisionerName: "eab-disabled", + want: false, + } + }, + "ok/eab-enabled": func(t *testing.T) test { + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "eab-enabled", name) + return &provisioner.MockProvisioner{ + MgetID: func() string { + return "provID" + }, + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "eab-enabled", + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + RequireEab: true, + }, + }, + }, + }, nil + }, + } + return test{ + adminDB: db, + auth: auth, + provisionerName: "eab-enabled", + want: true, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + adminDB: tc.adminDB, + acmeDB: nil, + } + got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName) + if (err != nil) != (tc.err != nil) { + t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err) + return + } + if tc.err != nil { + assert.Type(t, &linkedca.Provisioner{}, prov) + assert.Type(t, &admin.Error{}, err) + adminError, _ := err.(*admin.Error) + assert.Equals(t, tc.err.Type, adminError.Type) + assert.Equals(t, tc.err.Status, adminError.Status) + assert.Equals(t, tc.err.StatusCode(), adminError.StatusCode()) + assert.Equals(t, tc.err.Message, adminError.Message) + assert.Equals(t, tc.err.Detail, adminError.Detail) + return + } + if got != tc.want { + t.Errorf("Handler.provisionerHasEABEnabled() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { + type fields struct { + Reference string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "fail/reference-too-long", + fields: fields{ + Reference: strings.Repeat("A", 257), + }, + wantErr: true, + }, + { + name: "ok/empty-reference", + fields: fields{ + Reference: "", + }, + wantErr: false, + }, + { + name: "ok", + fields: fields{ + Reference: "my-eab-reference", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &CreateExternalAccountKeyRequest{ + Reference: tt.fields.Reference, + } + if err := r.Validate(); (err != nil) != tt.wantErr { + t.Errorf("CreateExternalAccountKeyRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestHandler_CreateExternalAccountKey(t *testing.T) { + type test struct { + ctx context.Context + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + return test{ + ctx: ctx, + statusCode: 501, + err: &admin.Error{ + Type: admin.ErrorNotImplementedType.String(), + Status: http.StatusNotImplemented, + Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm", + Detail: "not implemented", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + + req := httptest.NewRequest("POST", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + acmeResponder := NewACMEAdminResponder() + acmeResponder.CreateExternalAccountKey(w, req) + res := w.Result() + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + }) + } +} + +func TestHandler_DeleteExternalAccountKey(t *testing.T) { + type test struct { + ctx context.Context + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + chiCtx.URLParams.Add("id", "keyID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + return test{ + ctx: ctx, + statusCode: 501, + err: &admin.Error{ + Type: admin.ErrorNotImplementedType.String(), + Status: http.StatusNotImplemented, + Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm", + Detail: "not implemented", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + + req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + acmeResponder := NewACMEAdminResponder() + acmeResponder.DeleteExternalAccountKey(w, req) + res := w.Result() + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + }) + } +} + +func TestHandler_GetExternalAccountKeys(t *testing.T) { + type test struct { + ctx context.Context + statusCode int + req *http.Request + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("provisionerName", "provName") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + return test{ + ctx: ctx, + statusCode: 501, + req: req, + err: &admin.Error{ + Type: admin.ErrorNotImplementedType.String(), + Status: http.StatusNotImplemented, + Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm", + Detail: "not implemented", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + acmeResponder := NewACMEAdminResponder() + acmeResponder.GetExternalAccountKeys(w, req) + + res := w.Result() + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + }) + } +} diff --git a/authority/admin/api/admin.go b/authority/admin/api/admin.go index bf79ebcf..7aa66d0f 100644 --- a/authority/admin/api/admin.go +++ b/authority/admin/api/admin.go @@ -1,14 +1,32 @@ package api import ( + "context" "net/http" "github.com/go-chi/chi" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" ) +type adminAuthority interface { + LoadProvisionerByName(string) (provisioner.Interface, error) + GetProvisioners(cursor string, limit int) (provisioner.List, string, error) + IsAdminAPIEnabled() bool + LoadAdminByID(id string) (*linkedca.Admin, bool) + GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) + StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error + UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) + RemoveAdmin(ctx context.Context, id string) error + AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) + StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error + LoadProvisionerByID(id string) (provisioner.Interface, error) + UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error + RemoveProvisioner(ctx context.Context, id string) error +} + // CreateAdminRequest represents the body for a CreateAdmin request. type CreateAdminRequest struct { Subject string `json:"subject"` diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go new file mode 100644 index 00000000..8d223b52 --- /dev/null +++ b/authority/admin/api/admin_test.go @@ -0,0 +1,919 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type mockAdminAuthority struct { + MockLoadProvisionerByName func(name string) (provisioner.Interface, error) + MockGetProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) + MockRet1, MockRet2 interface{} // TODO: refactor the ret1/ret2 into those two + MockErr error + MockIsAdminAPIEnabled func() bool + MockLoadAdminByID func(id string) (*linkedca.Admin, bool) + MockGetAdmins func(cursor string, limit int) ([]*linkedca.Admin, string, error) + MockStoreAdmin func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error + MockUpdateAdmin func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) + MockRemoveAdmin func(ctx context.Context, id string) error + MockAuthorizeAdminToken func(r *http.Request, token string) (*linkedca.Admin, error) + MockStoreProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error + MockLoadProvisionerByID func(id string) (provisioner.Interface, error) + MockUpdateProvisioner func(ctx context.Context, nu *linkedca.Provisioner) error + MockRemoveProvisioner func(ctx context.Context, id string) error +} + +func (m *mockAdminAuthority) IsAdminAPIEnabled() bool { + if m.MockIsAdminAPIEnabled != nil { + return m.MockIsAdminAPIEnabled() + } + return m.MockRet1.(bool) +} + +func (m *mockAdminAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) { + if m.MockLoadProvisionerByName != nil { + return m.MockLoadProvisionerByName(name) + } + return m.MockRet1.(provisioner.Interface), m.MockErr +} + +func (m *mockAdminAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { + if m.MockGetProvisioners != nil { + return m.MockGetProvisioners(nextCursor, limit) + } + return m.MockRet1.(provisioner.List), m.MockRet2.(string), m.MockErr +} + +func (m *mockAdminAuthority) LoadAdminByID(id string) (*linkedca.Admin, bool) { + if m.MockLoadAdminByID != nil { + return m.MockLoadAdminByID(id) + } + return m.MockRet1.(*linkedca.Admin), m.MockRet2.(bool) +} + +func (m *mockAdminAuthority) GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) { + if m.MockGetAdmins != nil { + return m.MockGetAdmins(cursor, limit) + } + return m.MockRet1.([]*linkedca.Admin), m.MockRet2.(string), m.MockErr +} + +func (m *mockAdminAuthority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + if m.MockStoreAdmin != nil { + return m.MockStoreAdmin(ctx, adm, prov) + } + return m.MockErr +} + +func (m *mockAdminAuthority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + if m.MockUpdateAdmin != nil { + return m.MockUpdateAdmin(ctx, id, nu) + } + return m.MockRet1.(*linkedca.Admin), m.MockErr +} + +func (m *mockAdminAuthority) RemoveAdmin(ctx context.Context, id string) error { + if m.MockRemoveAdmin != nil { + return m.MockRemoveAdmin(ctx, id) + } + return m.MockErr +} + +func (m *mockAdminAuthority) AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) { + if m.MockAuthorizeAdminToken != nil { + return m.MockAuthorizeAdminToken(r, token) + } + return m.MockRet1.(*linkedca.Admin), m.MockErr +} + +func (m *mockAdminAuthority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { + if m.MockStoreProvisioner != nil { + return m.MockStoreProvisioner(ctx, prov) + } + return m.MockErr +} + +func (m *mockAdminAuthority) LoadProvisionerByID(id string) (provisioner.Interface, error) { + if m.MockLoadProvisionerByID != nil { + return m.MockLoadProvisionerByID(id) + } + return m.MockRet1.(provisioner.Interface), m.MockErr +} + +func (m *mockAdminAuthority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error { + if m.MockUpdateProvisioner != nil { + return m.MockUpdateProvisioner(ctx, nu) + } + return m.MockErr +} + +func (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) error { + if m.MockRemoveProvisioner != nil { + return m.MockRemoveProvisioner(ctx, id) + } + return m.MockErr +} + +func TestCreateAdminRequest_Validate(t *testing.T) { + type fields struct { + Subject string + Provisioner string + Type linkedca.Admin_Type + } + tests := []struct { + name string + fields fields + err *admin.Error + }{ + { + name: "fail/subject-empty", + fields: fields{ + Subject: "", + Provisioner: "", + Type: 0, + }, + err: admin.NewError(admin.ErrorBadRequestType, "subject cannot be empty"), + }, + { + name: "fail/provisioner-empty", + fields: fields{ + Subject: "admin", + Provisioner: "", + Type: 0, + }, + err: admin.NewError(admin.ErrorBadRequestType, "provisioner cannot be empty"), + }, + { + name: "fail/invalid-type", + fields: fields{ + Subject: "admin", + Provisioner: "prov", + Type: -1, + }, + err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"), + }, + { + name: "ok", + fields: fields{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + }, + err: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + car := &CreateAdminRequest{ + Subject: tt.fields.Subject, + Provisioner: tt.fields.Provisioner, + Type: tt.fields.Type, + } + err := car.Validate() + + if (err != nil) != (tt.err != nil) { + t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil)) + return + } + + if err != nil { + assert.Type(t, &admin.Error{}, err) + adminErr, _ := err.(*admin.Error) + assert.Equals(t, tt.err.Type, adminErr.Type) + assert.Equals(t, tt.err.Detail, adminErr.Detail) + assert.Equals(t, tt.err.Status, adminErr.Status) + assert.Equals(t, tt.err.Message, adminErr.Message) + } + }) + } +} + +func TestUpdateAdminRequest_Validate(t *testing.T) { + type fields struct { + Type linkedca.Admin_Type + } + tests := []struct { + name string + fields fields + err *admin.Error + }{ + { + name: "fail/invalid-type", + fields: fields{ + Type: -1, + }, + err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"), + }, + { + name: "ok", + fields: fields{ + Type: linkedca.Admin_SUPER_ADMIN, + }, + err: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uar := &UpdateAdminRequest{ + Type: tt.fields.Type, + } + + err := uar.Validate() + + if (err != nil) != (tt.err != nil) { + t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil)) + return + } + + if err != nil { + assert.Type(t, &admin.Error{}, err) + adminErr, _ := err.(*admin.Error) + assert.Equals(t, tt.err.Type, adminErr.Type) + assert.Equals(t, tt.err.Detail, adminErr.Detail) + assert.Equals(t, tt.err.Status, adminErr.Status) + assert.Equals(t, tt.err.Message, adminErr.Message) + } + }) + } +} + +func TestHandler_GetAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadAdminByID-not-found": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { + assert.Equals(t, "adminID", id) + return nil, false + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 404, + err: &admin.Error{ + Type: admin.ErrorNotFoundType.String(), + Status: 404, + Detail: "resource not found", + Message: "admin adminID not found", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + adm := &linkedca.Admin{ + Id: "adminID", + AuthorityId: "authorityID", + Subject: "admin", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &mockAdminAuthority{ + MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { + assert.Equals(t, "adminID", id) + return adm, true + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 200, + err: nil, + adm: adm, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetAdmin(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} + +func TestHandler_GetAdmins(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + req *http.Request + statusCode int + err *admin.Error + resp GetAdminsResponse + } + var tests = map[string]func(t *testing.T) test{ + "fail/parse-cursor": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo?limit=A", nil) + return test{ + ctx: context.Background(), + req: req, + statusCode: 400, + err: &admin.Error{ + Status: 400, + Type: admin.ErrorBadRequestType.String(), + Detail: "bad request", + Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax", + }, + } + }, + "fail/auth.GetAdmins": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + auth := &mockAdminAuthority{ + MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Status: 500, + Type: admin.ErrorServerInternalType.String(), + Detail: "the server experienced an internal error", + Message: "error retrieving paginated admins: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + createdAt := time.Now() + var deletedAt time.Time + adm1 := &linkedca.Admin{ + Id: "adminID1", + AuthorityId: "authorityID1", + Subject: "admin1", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + adm2 := &linkedca.Admin{ + Id: "adminID2", + AuthorityId: "authorityID", + Subject: "admin2", + ProvisionerId: "provID", + Type: linkedca.Admin_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &mockAdminAuthority{ + MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return []*linkedca.Admin{ + adm1, + adm2, + }, "nextCursorValue", nil + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 200, + err: nil, + resp: GetAdminsResponse{ + Admins: []*linkedca.Admin{ + adm1, + adm2, + }, + NextCursor: "nextCursorValue", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetAdmins(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + response := GetAdminsResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("GetAdmins diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + } + }) + } +} + +func TestHandler_CreateAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + body []byte + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string", + }, + } + }, + "fail/validate": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "", + Provisioner: "", + Type: -1, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "subject cannot be empty", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner prov: force", + }, + } + }, + "fail/auth.StoreAdmin": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return &provisioner.ACME{ + ID: "provID", + Name: "prov", + }, nil + }, + MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + assert.Equals(t, "admin", adm.Subject) + assert.Equals(t, "provID", prov.GetID()) + return errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error storing admin: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return &provisioner.ACME{ + ID: "provID", + Name: "prov", + }, nil + }, + MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + assert.Equals(t, "admin", adm.Subject) + assert.Equals(t, "provID", prov.GetID()) + return nil + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 201, + err: nil, + adm: &linkedca.Admin{ + ProvisionerId: "provID", + Subject: "admin", + Type: linkedca.Admin_SUPER_ADMIN, + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.CreateAdmin(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("h.CreateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} + +func TestHandler_DeleteAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.RemoveAdmin": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockRemoveAdmin: func(ctx context.Context, id string) error { + assert.Equals(t, "adminID", id) + return errors.New("force") + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error deleting admin adminID: force", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockRemoveAdmin: func(ctx context.Context, id string) error { + assert.Equals(t, "adminID", id) + return nil + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.DeleteAdmin(w, req) + res := w.Result() + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + }) + } +} + +func TestHandler_UpdateAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + body []byte + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string", + }, + } + }, + "fail/validate": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: -1, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "invalid value for admin type", + }, + } + }, + "fail/auth.UpdateAdmin": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: linkedca.Admin_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + assert.Equals(t, "adminID", id) + assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error updating admin adminID: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: linkedca.Admin_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + adm := &linkedca.Admin{ + Id: "adminID", + ProvisionerId: "provID", + Subject: "admin", + Type: linkedca.Admin_SUPER_ADMIN, + } + auth := &mockAdminAuthority{ + MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + assert.Equals(t, "adminID", id) + assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) + return adm, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 200, + err: nil, + adm: adm, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.UpdateAdmin(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("h.UpdateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index d88edfa1..99e74c88 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -1,22 +1,27 @@ package api import ( + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" - "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/admin" ) -// Handler is the ACME API request handler. +// Handler is the Admin API request handler. type Handler struct { - db admin.DB - auth *authority.Authority + adminDB admin.DB + auth adminAuthority + acmeDB acme.DB + acmeResponder acmeAdminResponderInterface } // NewHandler returns a new Authority Config Handler. -func NewHandler(auth *authority.Authority) api.RouterHandler { - h := &Handler{db: auth.GetAdminDatabase(), auth: auth} - - return h +func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler { + return &Handler{ + auth: auth, + adminDB: adminDB, + acmeDB: acmeDB, + acmeResponder: acmeResponder, + } } // Route traffic and implement the Router interface. @@ -25,6 +30,10 @@ func (h *Handler) Route(r api.Router) { return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) } + requireEABEnabled := func(next nextHTTP) nextHTTP { + return h.requireEABEnabled(next) + } + // Provisioners r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner)) r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners)) @@ -38,4 +47,10 @@ func (h *Handler) Route(r api.Router) { r.MethodFunc("POST", "/admins", authnz(h.CreateAdmin)) r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin)) r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin)) + + // ACME External Account Binding Keys + r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys))) + r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys))) + r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey))) + r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey))) } diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go new file mode 100644 index 00000000..7fb4671a --- /dev/null +++ b/authority/admin/api/middleware_test.go @@ -0,0 +1,225 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/admin" + "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestHandler_requireAPIEnabled(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + next nextHTTP + err *admin.Error + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.IsAdminAPIEnabled": func(t *testing.T) test { + return test{ + ctx: context.Background(), + auth: &mockAdminAuthority{ + MockIsAdminAPIEnabled: func() bool { + return false + }, + }, + err: &admin.Error{ + Type: admin.ErrorNotImplementedType.String(), + Status: 501, + Detail: "not implemented", + Message: "administration API not enabled", + }, + statusCode: 501, + } + }, + "ok": func(t *testing.T) test { + auth := &mockAdminAuthority{ + MockIsAdminAPIEnabled: func() bool { + return true + }, + } + next := func(w http.ResponseWriter, r *http.Request) { + w.Write(nil) // mock response with status 200 + } + return test{ + ctx: context.Background(), + auth: auth, + next: next, + statusCode: 200, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.requireAPIEnabled(tc.next)(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + err := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err)) + + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Message, err.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + // nothing to test when the requireAPIEnabled middleware succeeds, currently + + }) + } +} + +func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + req *http.Request + next nextHTTP + err *admin.Error + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/missing-authorization-token": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + req.Header["Authorization"] = []string{""} + return test{ + ctx: context.Background(), + req: req, + statusCode: 401, + err: &admin.Error{ + Type: admin.ErrorUnauthorizedType.String(), + Status: 401, + Detail: "unauthorized", + Message: "missing authorization header token", + }, + } + }, + "fail/auth.AuthorizeAdminToken": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + req.Header["Authorization"] = []string{"token"} + auth := &mockAdminAuthority{ + MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { + assert.Equals(t, "token", token) + return nil, admin.NewError( + admin.ErrorUnauthorizedType, + "not authorized", + ) + }, + } + return test{ + ctx: context.Background(), + auth: auth, + req: req, + statusCode: 401, + err: &admin.Error{ + Type: admin.ErrorUnauthorizedType.String(), + Status: 401, + Detail: "unauthorized", + Message: "not authorized", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + req.Header["Authorization"] = []string{"token"} + createdAt := time.Now() + var deletedAt time.Time + admin := &linkedca.Admin{ + Id: "adminID", + AuthorityId: "authorityID", + Subject: "admin", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &mockAdminAuthority{ + MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) { + assert.Equals(t, "token", token) + return admin, nil + }, + } + next := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + a := ctx.Value(adminContextKey) // verifying that the context now has a linkedca.Admin + adm, ok := a.(*linkedca.Admin) + if !ok { + t.Errorf("expected *linkedca.Admin; got %T", a) + return + } + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(admin, adm, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(admin, adm, opts...)) + } + w.Write(nil) // mock response with status 200 + } + return test{ + ctx: context.Background(), + auth: auth, + req: req, + next: next, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.extractAuthorizeTokenAdmin(tc.next)(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + if res.StatusCode >= 400 { + err := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err)) + + assert.Equals(t, tc.err.Type, err.Type) + assert.Equals(t, tc.err.Message, err.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, err.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + }) + } +} diff --git a/authority/admin/api/provisioner.go b/authority/admin/api/provisioner.go index fd1a02d5..b8cc0f4c 100644 --- a/authority/admin/api/provisioner.go +++ b/authority/admin/api/provisioner.go @@ -41,7 +41,7 @@ func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) { } } - prov, err := h.db.GetProvisioner(ctx, p.GetID()) + prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) if err != nil { api.WriteError(w, err) return @@ -54,7 +54,7 @@ func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) { cursor, limit, err := api.ParseCursor(r) if err != nil { api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, - "error parsing cursor & limit query params")) + "error parsing cursor and limit from query params")) return } @@ -134,7 +134,7 @@ func (h *Handler) UpdateProvisioner(w http.ResponseWriter, r *http.Request) { return } - old, err := h.db.GetProvisioner(r.Context(), _old.GetID()) + old, err := h.adminDB.GetProvisioner(r.Context(), _old.GetID()) if err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", _old.GetID())) return diff --git a/authority/admin/api/provisioner_test.go b/authority/admin/api/provisioner_test.go new file mode 100644 index 00000000..6d5024f2 --- /dev/null +++ b/authority/admin/api/provisioner_test.go @@ -0,0 +1,1100 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestHandler_GetProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + adminDB admin.DB + req *http.Request + statusCode int + err *admin.Error + prov *linkedca.Provisioner + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadProvisionerByID": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo?id=provID", nil) + chiCtx := chi.NewRouteContext() + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provID: force", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provName: force", + }, + } + }, + "fail/db.GetProvisioner": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.ACME{ + ID: "acmeID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "acmeID", id) + return nil, admin.NewErrorISE("error loading provisioner provName: force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provName: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.ACME{ + ID: "acmeID", + Name: "provName", + }, nil + }, + } + prov := &linkedca.Provisioner{ + Id: "acmeID", + Type: linkedca.Provisioner_ACME, + Name: "provName", // TODO(hs): other fields too? + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "acmeID", id) + return prov, nil + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + adminDB: db, + statusCode: 200, + err: nil, + prov: prov, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + adminDB: tc.adminDB, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + prov := &linkedca.Provisioner{} + err := readProtoJSON(res.Body, prov) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.prov, prov, opts...) { + t.Errorf("h.GetProvisioner diff =\n%s", cmp.Diff(tc.prov, prov, opts...)) + } + }) + } +} + +func TestHandler_GetProvisioners(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + req *http.Request + statusCode int + err *admin.Error + resp GetProvisionersResponse + } + var tests = map[string]func(t *testing.T) test{ + "fail/parse-cursor": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo?limit=X", nil) + return test{ + ctx: context.Background(), + statusCode: 400, + req: req, + err: &admin.Error{ + Status: 400, + Type: admin.ErrorBadRequestType.String(), + Detail: "bad request", + Message: "error parsing cursor and limit from query params: limit 'X' is not an integer: strconv.Atoi: parsing \"X\": invalid syntax", + }, + } + }, + "fail/auth.GetProvisioners": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + auth := &mockAdminAuthority{ + MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: "", + Status: 500, + Detail: "", + Message: "The certificate authority encountered an Internal Server Error. Please see the certificate authority logs for more info.", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + provisioners := provisioner.List{ + &provisioner.OIDC{ + Type: "OIDC", + Name: "oidcProv", + }, + &provisioner.ACME{ + Type: "ACME", + Name: "provName", + ForceCN: false, + RequireEAB: false, + }, + } + auth := &mockAdminAuthority{ + MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return provisioners, "nextCursorValue", nil + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 200, + err: nil, + resp: GetProvisionersResponse{ + Provisioners: provisioners, + NextCursor: "nextCursorValue", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetProvisioners(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := GetProvisionersResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(provisioner.ACME{}, provisioner.OIDC{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("h.GetProvisioners diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + } + }) + } +} + +func TestHandler_CreateProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + body []byte + statusCode int + err *admin.Error + prov *linkedca.Provisioner + } + var tests = map[string]func(t *testing.T) test{ + "fail/readProtoJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 500, + err: &admin.Error{ // TODO(hs): this probably needs a better error + Type: "", + Status: 500, + Detail: "", + Message: "", + }, + } + }, + // TODO(hs): ValidateClaims can't be mocked atm + // "fail/authority.ValidateClaims": func(t *testing.T) test { + // return test{} + // }, + "fail/auth.StoreProvisioner": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error { + assert.Equals(t, "provID", prov.Id) + return errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error storing provisioner provName: force", + }, + } + }, + "ok": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error { + assert.Equals(t, "provID", prov.Id) + return nil + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 201, + err: nil, + prov: prov, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.CreateProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + prov := &linkedca.Provisioner{} + err := readProtoJSON(res.Body, prov) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.prov, prov, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.prov, prov, opts...)) + } + }) + } +} + +func TestHandler_DeleteProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + req *http.Request + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadProvisionerByID": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo?id=provID", nil) + chiCtx := chi.NewRouteContext() + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provID: force", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner provName: force", + }, + } + }, + "fail/auth.RemoveProvisioner": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + Type: "OIDC", + }, nil + }, + MockRemoveProvisioner: func(ctx context.Context, id string) error { + assert.Equals(t, "provID", id) + return errors.New("force") + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error removing provisioner provName: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("DELETE", "/foo", nil) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + Type: "OIDC", + }, nil + }, + MockRemoveProvisioner: func(ctx context.Context, id string) error { + assert.Equals(t, "provID", id) + return nil + }, + } + return test{ + ctx: ctx, + req: req, + auth: auth, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.DeleteProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + }) + } +} + +func TestHandler_UpdateProvisioner(t *testing.T) { + type test struct { + ctx context.Context + auth adminAuthority + body []byte + adminDB admin.DB + statusCode int + err *admin.Error + prov *linkedca.Provisioner + } + var tests = map[string]func(t *testing.T) test{ + "fail/readProtoJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 500, + err: &admin.Error{ // TODO(hs): this probably needs a better error + Type: "", + Status: 500, + Detail: "", + Message: "", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner from cached configuration 'provName': force", + }, + } + }, + "fail/db.GetProvisioner": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner from db 'provID': force", + }, + } + }, + "fail/change-id-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "differentProvID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner ID", + }, + } + }, + "fail/change-type-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_JWK, + Name: "provName", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner type", + }, + } + }, + "fail/change-authority-id-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "differentAuthorityID", + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner authorityID", + }, + } + }, + "fail/change-createdAt-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(time.Now().Add(-1 * time.Hour)), + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner createdAt", + }, + } + }, + "fail/change-deletedAt-error": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(time.Now()), + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "cannot change provisioner deletedAt", + }, + } + }, + // TODO(hs): ValidateClaims can't be mocked atm + //"fail/ValidateClaims": func(t *testing.T) test { return test{} }, + "fail/auth.UpdateProvisioner": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + assert.Equals(t, "provID", nu.Id) + assert.Equals(t, "provName", nu.Name) + return errors.New("force") + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 500, + err: &admin.Error{ + Type: "", // TODO(hs): this error can be improved + Status: 500, + Detail: "", + Message: "", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("name", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + prov := &linkedca.Provisioner{ + Id: "provID", + Type: linkedca.Provisioner_OIDC, + Name: "provName", + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_OIDC{ + OIDC: &linkedca.OIDCProvisioner{ + ClientId: "new-client-id", + ClientSecret: "new-client-secret", + }, + }, + }, + } + body, err := protojson.Marshal(prov) + assert.FatalError(t, err) + auth := &mockAdminAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "provName", name) + return &provisioner.OIDC{ + ID: "provID", + Name: "provName", + }, nil + }, + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + assert.Equals(t, "provID", nu.Id) + assert.Equals(t, "provName", nu.Name) + return nil + }, + } + db := &admin.MockDB{ + MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) { + assert.Equals(t, "provID", id) + return &linkedca.Provisioner{ + Id: "provID", + Name: "provName", + Type: linkedca.Provisioner_OIDC, + AuthorityId: "authorityID", + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + }, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + adminDB: db, + statusCode: 200, + prov: prov, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + adminDB: tc.adminDB, + } + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.UpdateProvisioner(w, req) + res := w.Result() + + assert.Equals(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + prov := &linkedca.Provisioner{} + err := readProtoJSON(res.Body, prov) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{ + cmpopts.IgnoreUnexported( + linkedca.Provisioner{}, linkedca.ProvisionerDetails{}, linkedca.ProvisionerDetails_OIDC{}, + linkedca.OIDCProvisioner{}, timestamppb.Timestamp{}, + ), + } + if !cmp.Equal(tc.prov, prov, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.prov, prov, opts...)) + } + }) + } +} diff --git a/authority/admin/db/nosql/admin_test.go b/authority/admin/db/nosql/admin_test.go index 4234d526..2631b68c 100644 --- a/authority/admin/db/nosql/admin_test.go +++ b/authority/admin/db/nosql/admin_test.go @@ -11,7 +11,7 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" - "github.com/smallstep/nosql/database" + nosqldb "github.com/smallstep/nosql/database" "go.step.sm/linkedca" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -31,7 +31,7 @@ func TestDB_getDBAdminBytes(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -105,7 +105,7 @@ func TestDB_getDBAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -398,7 +398,7 @@ func TestDB_GetAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -551,7 +551,7 @@ func TestDB_DeleteAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -697,7 +697,7 @@ func TestDB_UpdateAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -985,7 +985,7 @@ func TestDB_GetAdmins(t *testing.T) { "fail/db.List-error": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return nil, errors.New("force") @@ -995,14 +995,14 @@ func TestDB_GetAdmins(t *testing.T) { } }, "fail/unmarshal-error": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: adminsTable, Key: []byte("foo"), Value: foob}, {Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("zap"), Value: []byte("zap")}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil @@ -1012,10 +1012,10 @@ func TestDB_GetAdmins(t *testing.T) { } }, "ok/none": func(t *testing.T) test { - ret := []*database.Entry{} + ret := []*nosqldb.Entry{} return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil @@ -1027,13 +1027,13 @@ func TestDB_GetAdmins(t *testing.T) { } }, "ok/only-invalid": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("baz"), Value: bazb}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil @@ -1045,7 +1045,7 @@ func TestDB_GetAdmins(t *testing.T) { } }, "ok": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: adminsTable, Key: []byte("foo"), Value: foob}, {Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("baz"), Value: bazb}, @@ -1053,7 +1053,7 @@ func TestDB_GetAdmins(t *testing.T) { } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, adminsTable) return ret, nil diff --git a/authority/admin/db/nosql/provisioner_test.go b/authority/admin/db/nosql/provisioner_test.go index e599ea04..a399558a 100644 --- a/authority/admin/db/nosql/provisioner_test.go +++ b/authority/admin/db/nosql/provisioner_test.go @@ -11,7 +11,7 @@ import ( "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" - "github.com/smallstep/nosql/database" + nosqldb "github.com/smallstep/nosql/database" "go.step.sm/linkedca" ) @@ -30,7 +30,7 @@ func TestDB_getDBProvisionerBytes(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -104,7 +104,7 @@ func TestDB_getDBProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -444,7 +444,7 @@ func TestDB_GetProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -581,7 +581,7 @@ func TestDB_DeleteProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -735,7 +735,7 @@ func TestDB_GetProvisioners(t *testing.T) { "fail/db.List-error": func(t *testing.T) test { return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return nil, errors.New("force") @@ -745,14 +745,14 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "fail/unmarshal-error": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: provisionersTable, Key: []byte("foo"), Value: foob}, {Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("zap"), Value: []byte("zap")}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil @@ -762,10 +762,10 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "ok/none": func(t *testing.T) test { - ret := []*database.Entry{} + ret := []*nosqldb.Entry{} return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil @@ -777,13 +777,13 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "ok/only-invalid": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("baz"), Value: bazb}, } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil @@ -795,7 +795,7 @@ func TestDB_GetProvisioners(t *testing.T) { } }, "ok": func(t *testing.T) test { - ret := []*database.Entry{ + ret := []*nosqldb.Entry{ {Bucket: provisionersTable, Key: []byte("foo"), Value: foob}, {Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("baz"), Value: bazb}, @@ -803,7 +803,7 @@ func TestDB_GetProvisioners(t *testing.T) { } return test{ db: &db.MockNoSQLDB{ - MList: func(bucket []byte) ([]*database.Entry, error) { + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { assert.Equals(t, bucket, provisionersTable) return ret, nil @@ -988,7 +988,7 @@ func TestDB_UpdateProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, database.ErrNotFound + return nil, nosqldb.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), diff --git a/authority/admin/errors.go b/authority/admin/errors.go index 607093b0..217227ca 100644 --- a/authority/admin/errors.go +++ b/authority/admin/errors.go @@ -104,7 +104,7 @@ var ( } ) -// Error represents an Admin +// Error represents an Admin error type Error struct { Type string `json:"type"` Detail string `json:"detail"` diff --git a/authority/authority.go b/authority/authority.go index b6fcdf23..b10c3c33 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -50,6 +50,7 @@ type Authority struct { rootX509CertPool *x509.CertPool federatedX509Certs []*x509.Certificate certificates *sync.Map + x509Enforcers []provisioner.CertificateEnforcer // SCEP CA scepService *scep.Service @@ -437,13 +438,6 @@ func (a *Authority) init() error { } } - // Check if a KMS with decryption capability is required and available - if a.requiresDecrypter() { - if _, ok := a.keyManager.(kmsapi.Decrypter); !ok { - return errors.New("keymanager doesn't provide crypto.Decrypter") - } - } - // TODO: decide if this is a good approach for providing the SCEP functionality // It currently mirrors the logic for the x509CAService if a.requiresSCEPService() && a.scepService == nil { @@ -454,6 +448,7 @@ func (a *Authority) init() error { if err != nil { return err } + options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...) options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.IntermediateKey, Password: []byte(a.password), diff --git a/authority/authority_test.go b/authority/authority_test.go index abb06cf4..1f63333d 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -6,7 +6,6 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" - "fmt" "net" "os" "reflect" @@ -328,7 +327,6 @@ func TestAuthority_CloseForReload(t *testing.T) { } func testScepAuthority(t *testing.T, opts ...Option) *Authority { - p := provisioner.List{ &provisioner.SCEP{ Name: "scep1", @@ -353,39 +351,15 @@ func testScepAuthority(t *testing.T, opts ...Option) *Authority { } func TestAuthority_GetSCEPService(t *testing.T) { - auth := testScepAuthority(t) - fmt.Println(auth) - + _ = testScepAuthority(t) p := provisioner.List{ &provisioner.SCEP{ Name: "scep1", Type: "SCEP", }, } - type fields struct { config *Config - // keyManager kms.KeyManager - // provisioners *provisioner.Collection - // db db.AuthDB - // templates *templates.Templates - // x509CAService cas.CertificateAuthorityService - // rootX509Certs []*x509.Certificate - // federatedX509Certs []*x509.Certificate - // certificates *sync.Map - // scepService *scep.Service - // sshCAUserCertSignKey ssh.Signer - // sshCAHostCertSignKey ssh.Signer - // sshCAUserCerts []ssh.PublicKey - // sshCAHostCerts []ssh.PublicKey - // sshCAUserFederatedCerts []ssh.PublicKey - // sshCAHostFederatedCerts []ssh.PublicKey - // initOnce bool - // startTime time.Time - // sshBastionFunc func(ctx context.Context, user, hostname string) (*Bastion, error) - // sshCheckHostFunc func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error) - // sshGetHostsFunc func(ctx context.Context, cert *x509.Certificate) ([]Host, error) - // getIdentityFunc provisioner.GetIdentityFunc } tests := []struct { name string @@ -434,30 +408,6 @@ func TestAuthority_GetSCEPService(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // a := &Authority{ - // config: tt.fields.config, - // keyManager: tt.fields.keyManager, - // provisioners: tt.fields.provisioners, - // db: tt.fields.db, - // templates: tt.fields.templates, - // x509CAService: tt.fields.x509CAService, - // rootX509Certs: tt.fields.rootX509Certs, - // federatedX509Certs: tt.fields.federatedX509Certs, - // certificates: tt.fields.certificates, - // scepService: tt.fields.scepService, - // sshCAUserCertSignKey: tt.fields.sshCAUserCertSignKey, - // sshCAHostCertSignKey: tt.fields.sshCAHostCertSignKey, - // sshCAUserCerts: tt.fields.sshCAUserCerts, - // sshCAHostCerts: tt.fields.sshCAHostCerts, - // sshCAUserFederatedCerts: tt.fields.sshCAUserFederatedCerts, - // sshCAHostFederatedCerts: tt.fields.sshCAHostFederatedCerts, - // initOnce: tt.fields.initOnce, - // startTime: tt.fields.startTime, - // sshBastionFunc: tt.fields.sshBastionFunc, - // sshCheckHostFunc: tt.fields.sshCheckHostFunc, - // sshGetHostsFunc: tt.fields.sshGetHostsFunc, - // getIdentityFunc: tt.fields.getIdentityFunc, - // } a, err := New(tt.fields.config) if (err != nil) != tt.wantErr { t.Errorf("Authority.New(), error = %v, wantErr %v", err, tt.wantErr) diff --git a/authority/config/config.go b/authority/config/config.go index 4e7a7b25..9fada6f1 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -274,28 +274,36 @@ func (c *Config) GetAudiences() provisioner.Audiences { for _, name := range c.DNSNames { audiences.Sign = append(audiences.Sign, - fmt.Sprintf("https://%s/1.0/sign", name), - fmt.Sprintf("https://%s/sign", name), - fmt.Sprintf("https://%s/1.0/ssh/sign", name), - fmt.Sprintf("https://%s/ssh/sign", name)) + fmt.Sprintf("https://%s/1.0/sign", toHostname(name)), + fmt.Sprintf("https://%s/sign", toHostname(name)), + fmt.Sprintf("https://%s/1.0/ssh/sign", toHostname(name)), + fmt.Sprintf("https://%s/ssh/sign", toHostname(name))) audiences.Revoke = append(audiences.Revoke, - fmt.Sprintf("https://%s/1.0/revoke", name), - fmt.Sprintf("https://%s/revoke", name)) + fmt.Sprintf("https://%s/1.0/revoke", toHostname(name)), + fmt.Sprintf("https://%s/revoke", toHostname(name))) audiences.SSHSign = append(audiences.SSHSign, - fmt.Sprintf("https://%s/1.0/ssh/sign", name), - fmt.Sprintf("https://%s/ssh/sign", name), - fmt.Sprintf("https://%s/1.0/sign", name), - fmt.Sprintf("https://%s/sign", name)) + fmt.Sprintf("https://%s/1.0/ssh/sign", toHostname(name)), + fmt.Sprintf("https://%s/ssh/sign", toHostname(name)), + fmt.Sprintf("https://%s/1.0/sign", toHostname(name)), + fmt.Sprintf("https://%s/sign", toHostname(name))) audiences.SSHRevoke = append(audiences.SSHRevoke, - fmt.Sprintf("https://%s/1.0/ssh/revoke", name), - fmt.Sprintf("https://%s/ssh/revoke", name)) + fmt.Sprintf("https://%s/1.0/ssh/revoke", toHostname(name)), + fmt.Sprintf("https://%s/ssh/revoke", toHostname(name))) audiences.SSHRenew = append(audiences.SSHRenew, - fmt.Sprintf("https://%s/1.0/ssh/renew", name), - fmt.Sprintf("https://%s/ssh/renew", name)) + fmt.Sprintf("https://%s/1.0/ssh/renew", toHostname(name)), + fmt.Sprintf("https://%s/ssh/renew", toHostname(name))) audiences.SSHRekey = append(audiences.SSHRekey, - fmt.Sprintf("https://%s/1.0/ssh/rekey", name), - fmt.Sprintf("https://%s/ssh/rekey", name)) + fmt.Sprintf("https://%s/1.0/ssh/rekey", toHostname(name)), + fmt.Sprintf("https://%s/ssh/rekey", toHostname(name))) } return audiences } + +func toHostname(name string) string { + // ensure an IPv6 address is represented with square brackets when used as hostname + if ip := net.ParseIP(name); ip != nil && ip.To4() == nil { + name = "[" + name + "]" + } + return name +} diff --git a/authority/config/config_test.go b/authority/config/config_test.go index a5b60513..b921be13 100644 --- a/authority/config/config_test.go +++ b/authority/config/config_test.go @@ -7,9 +7,8 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/crypto/jose" - _ "github.com/smallstep/certificates/cas" + "go.step.sm/crypto/jose" ) func TestConfigValidate(t *testing.T) { @@ -298,3 +297,23 @@ func TestAuthConfigValidate(t *testing.T) { }) } } + +func Test_toHostname(t *testing.T) { + tests := []struct { + name string + want string + }{ + {name: "localhost", want: "localhost"}, + {name: "ca.smallstep.com", want: "ca.smallstep.com"}, + {name: "127.0.0.1", want: "127.0.0.1"}, + {name: "::1", want: "[::1]"}, + {name: "[::1]", want: "[::1]"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toHostname(tt.name); got != tt.want { + t.Errorf("toHostname() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/options.go b/authority/options.go index a18d40e2..f92db99b 100644 --- a/authority/options.go +++ b/authority/options.go @@ -241,6 +241,15 @@ func WithLinkedCAToken(token string) Option { } } +// WithX509Enforcers is an option that allows to define custom certificate +// modifiers that will be processed just before the signing of the certificate. +func WithX509Enforcers(ces ...provisioner.CertificateEnforcer) Option { + return func(a *Authority) error { + a.x509Enforcers = ces + return nil + } +} + func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) { var block *pem.Block var certs []*x509.Certificate diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index c8950568..21958d36 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -13,13 +13,18 @@ import ( // provisioning flow. type ACME struct { *base - ID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - ForceCN bool `json:"forceCN,omitempty"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` - claimer *Claimer + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + ForceCN bool `json:"forceCN,omitempty"` + // RequireEAB makes the provisioner require ACME EAB to be provided + // by clients when creating a new Account. If set to true, the provided + // EAB will be verified. If set to false and an EAB is provided, it is + // not verified. Defaults to false. + RequireEAB bool `json:"requireEAB,omitempty"` + Claims *Claims `json:"claims,omitempty"` + Options *Options `json:"options,omitempty"` + claimer *Claimer } // GetID returns the provisioner unique identifier. diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 145a1920..5d67762c 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -18,13 +18,21 @@ type SCEP struct { ForceCN bool `json:"forceCN,omitempty"` ChallengePassword string `json:"challenge,omitempty"` Capabilities []string `json:"capabilities,omitempty"` + // IncludeRoot makes the provisioner return the CA root in addition to the + // intermediate in the GetCACerts response + IncludeRoot bool `json:"includeRoot,omitempty"` // MinimumPublicKeyLength is the minimum length for public keys in CSRs - MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"` - Options *Options `json:"options,omitempty"` - Claims *Claims `json:"claims,omitempty"` - claimer *Claimer + MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"` + // Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7 + // at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63 + // Defaults to 0, being DES-CBC + EncryptionAlgorithmIdentifier int `json:"encryptionAlgorithmIdentifier,omitempty"` + Options *Options `json:"options,omitempty"` + Claims *Claims `json:"claims,omitempty"` + claimer *Claimer secretChallengePassword string + encryptionAlgorithm int } // GetID returns the provisioner unique identifier. @@ -97,7 +105,12 @@ func (s *SCEP) Init(config Config) (err error) { } if s.MinimumPublicKeyLength%8 != 0 { - return errors.Errorf("only minimum public keys exactly divisible by 8 are supported; %d is not exactly divisible by 8", s.MinimumPublicKeyLength) + return errors.Errorf("%d bits is not exactly divisible by 8", s.MinimumPublicKeyLength) + } + + s.encryptionAlgorithm = s.EncryptionAlgorithmIdentifier // TODO(hs): we might want to upgrade the default security to AES-CBC? + if s.encryptionAlgorithm < 0 || s.encryptionAlgorithm > 4 { + return errors.New("only encryption algorithm identifiers from 0 to 4 are valid") } // TODO: add other, SCEP specific, options? @@ -129,3 +142,17 @@ func (s *SCEP) GetChallengePassword() string { func (s *SCEP) GetCapabilities() []string { return s.Capabilities } + +// ShouldIncludeRootInChain indicates if the CA should +// return its intermediate, which is currently used for +// both signing and decryption, as well as the root in +// its chain. +func (s *SCEP) ShouldIncludeRootInChain() bool { + return s.IncludeRoot +} + +// GetContentEncryptionAlgorithm returns the numeric identifier +// for the pkcs7 package encryption algorithm to use. +func (s *SCEP) GetContentEncryptionAlgorithm() int { + return s.encryptionAlgorithm +} diff --git a/authority/provisioners.go b/authority/provisioners.go index a98b78a6..3b14657c 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -638,12 +638,13 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, case *linkedca.ProvisionerDetails_ACME: cfg := d.ACME return &provisioner.ACME{ - ID: p.Id, - Type: p.Type.String(), - Name: p.Name, - ForceCN: cfg.ForceCn, - Claims: claims, - Options: options, + ID: p.Id, + Type: p.Type.String(), + Name: p.Name, + ForceCN: cfg.ForceCn, + RequireEAB: cfg.RequireEab, + Claims: claims, + Options: options, }, nil case *linkedca.ProvisionerDetails_OIDC: cfg := d.OIDC @@ -711,6 +712,21 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, Claims: claims, Options: options, }, nil + case *linkedca.ProvisionerDetails_SCEP: + cfg := d.SCEP + return &provisioner.SCEP{ + ID: p.Id, + Type: p.Type.String(), + Name: p.Name, + ForceCN: cfg.ForceCn, + ChallengePassword: cfg.Challenge, + Capabilities: cfg.Capabilities, + IncludeRoot: cfg.IncludeRoot, + MinimumPublicKeyLength: int(cfg.MinimumPublicKeyLength), + EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier), + Claims: claims, + Options: options, + }, nil case *linkedca.ProvisionerDetails_Nebula: var roots []byte for i, root := range d.Nebula.GetRoots() { @@ -943,10 +959,12 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Details: &linkedca.ProvisionerDetails{ Data: &linkedca.ProvisionerDetails_SCEP{ SCEP: &linkedca.SCEPProvisioner{ - ForceCn: p.ForceCN, - Challenge: p.GetChallengePassword(), - Capabilities: p.Capabilities, - MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength), + ForceCn: p.ForceCN, + Challenge: p.GetChallengePassword(), + Capabilities: p.Capabilities, + MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength), + IncludeRoot: p.IncludeRoot, + EncryptionAlgorithmIdentifier: int32(p.EncryptionAlgorithmIdentifier), }, }, }, diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 2ea65352..c299b347 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -57,7 +57,7 @@ func (m sshTestCertModifier) Modify(cert *ssh.Certificate, opts provisioner.Sign if m == "" { return nil } - return fmt.Errorf(string(m)) + return errors.New(string(m)) } type sshTestCertValidator string @@ -66,7 +66,7 @@ func (v sshTestCertValidator) Valid(crt *ssh.Certificate, opts provisioner.SignS if v == "" { return nil } - return fmt.Errorf(string(v)) + return errors.New(string(v)) } type sshTestOptionsValidator string @@ -75,7 +75,7 @@ func (v sshTestOptionsValidator) Valid(opts provisioner.SignSSHOptions) error { if v == "" { return nil } - return fmt.Errorf(string(v)) + return errors.New(string(v)) } type sshTestOptionsModifier string @@ -84,7 +84,7 @@ func (m sshTestOptionsModifier) Modify(cert *ssh.Certificate, opts provisioner.S if m == "" { return nil } - return fmt.Errorf(string(m)) + return errors.New(string(m)) } func TestAuthority_initHostOnly(t *testing.T) { diff --git a/authority/tls.go b/authority/tls.go index f6cd34c3..eb2b0001 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -10,6 +10,7 @@ import ( "encoding/json" "encoding/pem" "fmt" + "net" "net/http" "strings" "time" @@ -180,6 +181,17 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } } + // Process injected modifiers after validation + for _, m := range a.x509Enforcers { + if err := m.Enforce(leaf); err != nil { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(err, "error creating certificate"), + opts..., + ) + } + } + + // Sign certificate lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ Template: leaf, @@ -508,6 +520,17 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { return fatal(errors.New("private key is not a crypto.Signer")) } + // prepare the sans: IPv6 DNS hostname representations are converted to their IP representation + sans := make([]string, len(a.config.DNSNames)) + for i, san := range a.config.DNSNames { + if strings.HasPrefix(san, "[") && strings.HasSuffix(san, "]") { + if ip := net.ParseIP(san[1 : len(san)-1]); ip != nil { + san = ip.String() + } + } + sans[i] = san + } + // Create initial certificate request. cr, err := x509util.CreateCertificateRequest(a.config.CommonName, a.config.DNSNames, signer) if err != nil { diff --git a/authority/tls_test.go b/authority/tls_test.go index 3a0c999e..aeadaf0f 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -205,6 +205,17 @@ type basicConstraints struct { MaxPathLen int `asn1:"optional,default:-1"` } +type testEnforcer struct { + enforcer func(*x509.Certificate) error +} + +func (e *testEnforcer) Enforce(cert *x509.Certificate) error { + if e.enforcer != nil { + return e.enforcer(cert) + } + return nil +} + func TestAuthority_Sign(t *testing.T) { pub, priv, err := keyutil.GenerateDefaultKeyPair() assert.FatalError(t, err) @@ -238,14 +249,15 @@ func TestAuthority_Sign(t *testing.T) { assert.FatalError(t, err) type signTest struct { - auth *Authority - csr *x509.CertificateRequest - signOpts provisioner.SignOptions - extraOpts []provisioner.SignOption - notBefore time.Time - notAfter time.Time - err error - code int + auth *Authority + csr *x509.CertificateRequest + signOpts provisioner.SignOptions + extraOpts []provisioner.SignOption + notBefore time.Time + notAfter time.Time + extensionsCount int + err error + code int } tests := map[string]func(*testing.T) *signTest{ "fail invalid signature": func(t *testing.T) *signTest { @@ -454,22 +466,66 @@ ZYtQ9Ot36qc= code: http.StatusInternalServerError, } }, - "ok": func(t *testing.T) *signTest { + "fail with provisioner enforcer": func(t *testing.T) *signTest { csr := getCSR(t, priv) - _a := testAuthority(t) - _a.db = &db.MockAuthDB{ + aa := testAuthority(t) + aa.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { assert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, } + return &signTest{ - auth: a, + auth: aa, + csr: csr, + extraOpts: append(extraOpts, &testEnforcer{ + enforcer: func(crt *x509.Certificate) error { return fmt.Errorf("an error") }, + }), + signOpts: signOpts, + err: errors.New("error creating certificate"), + code: http.StatusForbidden, + } + }, + "fail with custom enforcer": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + aa := testAuthority(t, WithX509Enforcers(&testEnforcer{ + enforcer: func(cert *x509.Certificate) error { + return fmt.Errorf("an error") + }, + })) + aa.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { + assert.Equals(t, crt.Subject.CommonName, "smallstep test") + return nil + }, + } + return &signTest{ + auth: aa, csr: csr, extraOpts: extraOpts, signOpts: signOpts, - notBefore: signOpts.NotBefore.Time().Truncate(time.Second), - notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + err: errors.New("error creating certificate"), + code: http.StatusForbidden, + } + }, + "ok": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + _a := testAuthority(t) + _a.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { + assert.Equals(t, crt.Subject.CommonName, "smallstep test") + return nil + }, + } + return &signTest{ + auth: a, + csr: csr, + extraOpts: extraOpts, + signOpts: signOpts, + notBefore: signOpts.NotBefore.Time().Truncate(time.Second), + notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + extensionsCount: 6, } }, "ok with enforced modifier": func(t *testing.T) *signTest { @@ -497,12 +553,13 @@ ZYtQ9Ot36qc= }, } return &signTest{ - auth: a, - csr: csr, - extraOpts: enforcedExtraOptions, - signOpts: signOpts, - notBefore: now.Truncate(time.Second), - notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second), + auth: a, + csr: csr, + extraOpts: enforcedExtraOptions, + signOpts: signOpts, + notBefore: now.Truncate(time.Second), + notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second), + extensionsCount: 6, } }, "ok with custom template": func(t *testing.T) *signTest { @@ -530,12 +587,13 @@ ZYtQ9Ot36qc= }, } return &signTest{ - auth: testAuthority, - csr: csr, - extraOpts: testExtraOpts, - signOpts: signOpts, - notBefore: signOpts.NotBefore.Time().Truncate(time.Second), - notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + auth: testAuthority, + csr: csr, + extraOpts: testExtraOpts, + signOpts: signOpts, + notBefore: signOpts.NotBefore.Time().Truncate(time.Second), + notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + extensionsCount: 6, } }, "ok/csr with no template critical SAN extension": func(t *testing.T) *signTest { @@ -558,12 +616,39 @@ ZYtQ9Ot36qc= }, } return &signTest{ - auth: _a, - csr: csr, - extraOpts: enforcedExtraOptions, - signOpts: provisioner.SignOptions{}, - notBefore: now.Truncate(time.Second), - notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second), + auth: _a, + csr: csr, + extraOpts: enforcedExtraOptions, + signOpts: provisioner.SignOptions{}, + notBefore: now.Truncate(time.Second), + notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second), + extensionsCount: 5, + } + }, + "ok with custom enforcer": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + aa := testAuthority(t, WithX509Enforcers(&testEnforcer{ + enforcer: func(cert *x509.Certificate) error { + cert.CRLDistributionPoints = []string{"http://ca.example.org/leaf.crl"} + return nil + }, + })) + aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template + aa.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { + assert.Equals(t, crt.Subject.CommonName, "smallstep test") + assert.Equals(t, crt.CRLDistributionPoints, []string{"http://ca.example.org/leaf.crl"}) + return nil + }, + } + return &signTest{ + auth: aa, + csr: csr, + extraOpts: extraOpts, + signOpts: signOpts, + notBefore: signOpts.NotBefore.Time().Truncate(time.Second), + notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + extensionsCount: 7, } }, } @@ -645,9 +730,6 @@ ZYtQ9Ot36qc= // Empty CSR subject test does not use any provisioner extensions. // So provisioner ID ext will be missing. found = 1 - assert.Len(t, 5, leaf.Extensions) - } else { - assert.Len(t, 6, leaf.Extensions) } } } @@ -655,6 +737,7 @@ ZYtQ9Ot36qc= realIntermediate, err := x509.ParseCertificate(issuer.Raw) assert.FatalError(t, err) assert.Equals(t, intermediate, realIntermediate) + assert.Len(t, tc.extensionsCount, leaf.Extensions) } } }) diff --git a/ca/adminClient.go b/ca/adminClient.go index 2e447f55..5f3993b1 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -12,7 +12,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/authority/admin" adminAPI "github.com/smallstep/certificates/authority/admin/api" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" @@ -40,6 +39,19 @@ type AdminClient struct { x5cSubject string } +// AdminClientError is the client side representation of an +// AdminError returned by the CA. +type AdminClientError struct { + Type string `json:"type"` + Detail string `json:"detail"` + Message string `json:"message"` +} + +// Error returns the AdminClientError message as the error message +func (e *AdminClientError) Error() string { + return e.Message +} + // NewAdminClient creates a new AdminClient with the given endpoint and options. func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) { u, err := parseEndpoint(endpoint) @@ -560,11 +572,119 @@ retry: return nil } +// GetExternalAccountKeysPaginate returns a page from the GET /admin/acme/eab request to the CA. +func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { + var retried bool + o := new(adminOptions) + if err := o.apply(opts); err != nil { + return nil, err + } + p := path.Join(adminURLPrefix, "acme/eab", provisionerName) + if reference != "" { + p = path.Join(p, "/", reference) + } + u := c.endpoint.ResolveReference(&url.URL{ + Path: p, + RawQuery: o.rawQuery(), + }) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, errors.Wrapf(err, "error generating admin token") + } + req, err := http.NewRequest("GET", u.String(), http.NoBody) + if err != nil { + return nil, errors.Wrapf(err, "create GET %s request failed", u) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var body = new(adminAPI.GetExternalAccountKeysResponse) + if err := readJSON(resp.Body, body); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return body, nil +} + +// CreateExternalAccountKey performs the POST /admin/acme/eab request to the CA. +func (c *AdminClient) CreateExternalAccountKey(provisionerName string, eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*linkedca.EABKey, error) { + var retried bool + body, err := json.Marshal(eakRequest) + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab/", provisionerName)}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, errors.Wrapf(err, "error generating admin token") + } + req, err := http.NewRequest("POST", u.String(), bytes.NewReader(body)) + if err != nil { + return nil, errors.Wrapf(err, "create POST %s request failed", u) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "client POST %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var eabKey = new(linkedca.EABKey) + if err := readProtoJSON(resp.Body, eabKey); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return eabKey, nil +} + +// RemoveExternalAccountKey performs the DELETE /admin/acme/eab/{prov}/{key_id} request to the CA. +func (c *AdminClient) RemoveExternalAccountKey(provisionerName, keyID string) error { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", provisionerName, "/", keyID)}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return errors.Wrapf(err, "error generating admin token") + } + req, err := http.NewRequest("DELETE", u.String(), http.NoBody) + if err != nil { + return errors.Wrapf(err, "create DELETE %s request failed", u) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return errors.Wrapf(err, "client DELETE %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readAdminError(resp.Body) + } + return nil +} + func readAdminError(r io.ReadCloser) error { + // TODO: not all errors can be read (i.e. 404); seems to be a bigger issue defer r.Close() - adminErr := new(admin.Error) + adminErr := new(AdminClientError) if err := json.NewDecoder(r).Decode(adminErr); err != nil { return err } - return errors.New(adminErr.Message) + return adminErr } diff --git a/ca/ca.go b/ca/ca.go index da0fb874..c95ba22f 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -207,7 +207,8 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { if cfg.AuthorityConfig.EnableAdmin { adminDB := auth.GetAdminDatabase() if adminDB != nil { - adminHandler := adminAPI.NewHandler(auth) + acmeAdminResponder := adminAPI.NewACMEAdminResponder() + adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder) mux.Route("/admin", func(r chi.Router) { adminHandler.Route(r) }) @@ -417,11 +418,6 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) { } } - certPool := x509.NewCertPool() - for _, crt := range auth.GetRootCertificates() { - certPool.AddCert(crt) - } - // GetCertificate will only be called if the client supplies SNI // information or if tlsConfig.Certificates is empty. // When client requests are made using an IP address (as opposed to a domain @@ -432,6 +428,24 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) { tlsConfig.Certificates = []tls.Certificate{} tlsConfig.GetCertificate = ca.renewer.GetCertificateForCA + // initialize a certificate pool with root CA certificates to trust when doing mTLS. + certPool := x509.NewCertPool() + for _, crt := range auth.GetRootCertificates() { + certPool.AddCert(crt) + } + + // adding the intermediate CA certificates to the pool will allow clients that + // do mTLS but don't send an intermediate to successfully connect. The intermediates + // added here are used when building a certificate chain. + intermediates := tlsCrt.Certificate[1:] + for _, certBytes := range intermediates { + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, err + } + certPool.AddCert(cert) + } + // Add support for mutual tls to renew certificates tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven tlsConfig.ClientCAs = certPool diff --git a/ca/provisioner.go b/ca/provisioner.go index 25e5e8ae..c1879c86 100644 --- a/ca/provisioner.go +++ b/ca/provisioner.go @@ -155,11 +155,11 @@ func (p *Provisioner) SSHToken(certType, keyID string, principals []string) (str func decryptProvisionerJWK(encryptedKey string, password []byte) (*jose.JSONWebKey, error) { enc, err := jose.ParseEncrypted(encryptedKey) if err != nil { - return nil, err + return nil, errors.Wrap(err, "error parsing provisioner encrypted key") } data, err := enc.Decrypt(password) if err != nil { - return nil, err + return nil, errors.Wrap(err, "error decrypting provisioner key with provided password") } jwk := new(jose.JSONWebKey) if err := json.Unmarshal(data, jwk); err != nil { diff --git a/ca/provisioner_test.go b/ca/provisioner_test.go index 01b54d17..39193f3f 100644 --- a/ca/provisioner_test.go +++ b/ca/provisioner_test.go @@ -200,6 +200,102 @@ func TestProvisioner_Token(t *testing.T) { } } +func TestProvisioner_IPv6Token(t *testing.T) { + p := getTestProvisioner(t, "https://[::1]:9000") + sha := "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7" + + type fields struct { + name string + kid string + fingerprint string + jwk *jose.JSONWebKey + tokenLifetime time.Duration + } + type args struct { + subject string + sans []string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + {"ok", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"subject", nil}, false}, + {"ok-with-san", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"subject", []string{"foo.smallstep.com"}}, false}, + {"ok-with-sans", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"subject", []string{"foo.smallstep.com", "127.0.0.1"}}, false}, + {"fail-no-subject", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{"", []string{"foo.smallstep.com"}}, true}, + {"fail-no-key", fields{p.name, p.kid, sha, &jose.JSONWebKey{}, p.tokenLifetime}, args{"subject", nil}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Provisioner{ + name: tt.fields.name, + kid: tt.fields.kid, + audience: "https://[::1]:9000/1.0/sign", + fingerprint: tt.fields.fingerprint, + jwk: tt.fields.jwk, + tokenLifetime: tt.fields.tokenLifetime, + } + got, err := p.Token(tt.args.subject, tt.args.sans...) + if (err != nil) != tt.wantErr { + t.Errorf("Provisioner.Token() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr == false { + jwt, err := jose.ParseSigned(got) + if err != nil { + t.Error(err) + return + } + var claims jose.Claims + if err := jwt.Claims(tt.fields.jwk.Public(), &claims); err != nil { + t.Error(err) + return + } + if err := claims.ValidateWithLeeway(jose.Expected{ + Audience: []string{"https://[::1]:9000/1.0/sign"}, + Issuer: tt.fields.name, + Subject: tt.args.subject, + Time: time.Now().UTC(), + }, time.Minute); err != nil { + t.Error(err) + return + } + lifetime := claims.Expiry.Time().Sub(claims.NotBefore.Time()) + if lifetime != tt.fields.tokenLifetime { + t.Errorf("Claims token life time = %s, want %s", lifetime, tt.fields.tokenLifetime) + } + allClaims := make(map[string]interface{}) + if err := jwt.Claims(tt.fields.jwk.Public(), &allClaims); err != nil { + t.Error(err) + return + } + if v, ok := allClaims["sha"].(string); !ok || v != sha { + t.Errorf("Claim sha = %s, want %s", v, sha) + } + if len(tt.args.sans) == 0 { + if v, ok := allClaims["sans"].([]interface{}); !ok || !reflect.DeepEqual(v, []interface{}{tt.args.subject}) { + t.Errorf("Claim sans = %s, want %s", v, []interface{}{tt.args.subject}) + } + } else { + want := []interface{}{} + for _, s := range tt.args.sans { + want = append(want, s) + } + if v, ok := allClaims["sans"].([]interface{}); !ok || !reflect.DeepEqual(v, want) { + t.Errorf("Claim sans = %s, want %s", v, want) + } + } + if v, ok := allClaims["jti"].(string); !ok || v == "" { + t.Errorf("Claim jti = %s, want not blank", v) + } + } + }) + } +} + func TestProvisioner_SSHToken(t *testing.T) { p := getTestProvisioner(t, "https://127.0.0.1:9000") sha := "ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7" diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index 7f996c15..1a16ef1e 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -31,6 +31,7 @@ import ( longrunningpb "google.golang.org/genproto/googleapis/longrunning" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/types/known/anypb" @@ -852,7 +853,7 @@ func TestCloudCAS_CreateCertificateAuthority(t *testing.T) { defer srv.Stop() // Create fake privateca client - conn, err := grpc.DialContext(context.Background(), "", grpc.WithInsecure(), + conn, err := grpc.DialContext(context.Background(), "", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() })) diff --git a/cmd/step-awskms-init/main.go b/cmd/step-awskms-init/main.go index 8e30745f..5e56f045 100644 --- a/cmd/step-awskms-init/main.go +++ b/cmd/step-awskms-init/main.go @@ -75,10 +75,11 @@ This tool is experimental and in the future it will be integrated in step cli. OPTIONS`) fmt.Fprintln(os.Stderr) flag.PrintDefaults() - fmt.Fprintln(os.Stderr, ` + fmt.Fprintf(os.Stderr, ` COPYRIGHT - (c) 2018-2020 Smallstep Labs, Inc.`) + (c) 2018-%d Smallstep Labs, Inc. +`, time.Now().Year()) os.Exit(1) } diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 9b02d892..f976b4b4 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -145,7 +145,7 @@ $ step-ca $STEPPATH/config/ca.json --password-file ./password.txt '''` app.Flags = append(app.Flags, commands.AppCommand.Flags...) app.Flags = append(app.Flags, cli.HelpFlag) - app.Copyright = "(c) 2018-2020 Smallstep Labs, Inc." + app.Copyright = fmt.Sprintf("(c) 2018-%d Smallstep Labs, Inc.", time.Now().Year()) // All non-successful output should be written to stderr app.Writer = os.Stdout diff --git a/cmd/step-cloudkms-init/main.go b/cmd/step-cloudkms-init/main.go index 27dc82ad..78c1fcc5 100644 --- a/cmd/step-cloudkms-init/main.go +++ b/cmd/step-cloudkms-init/main.go @@ -105,10 +105,11 @@ This tool is experimental and in the future it will be integrated in step cli. OPTIONS`) fmt.Fprintln(os.Stderr) flag.PrintDefaults() - fmt.Fprintln(os.Stderr, ` + fmt.Fprintf(os.Stderr, ` COPYRIGHT - (c) 2018-2020 Smallstep Labs, Inc.`) + (c) 2018-%d Smallstep Labs, Inc. +`, time.Now().Year()) os.Exit(1) } diff --git a/cmd/step-pkcs11-init/main.go b/cmd/step-pkcs11-init/main.go index 5fadf10d..4dc15799 100644 --- a/cmd/step-pkcs11-init/main.go +++ b/cmd/step-pkcs11-init/main.go @@ -250,10 +250,11 @@ This tool is experimental and in the future it will be integrated in step cli. OPTIONS`) fmt.Fprintln(os.Stderr) flag.PrintDefaults() - fmt.Fprintln(os.Stderr, ` + fmt.Fprintf(os.Stderr, ` COPYRIGHT - (c) 2018-2021 Smallstep Labs, Inc.`) + (c) 2018-%d Smallstep Labs, Inc. +`, time.Now().Year()) os.Exit(1) } diff --git a/cmd/step-yubikey-init/main.go b/cmd/step-yubikey-init/main.go index 8b0ffab5..9f755388 100644 --- a/cmd/step-yubikey-init/main.go +++ b/cmd/step-yubikey-init/main.go @@ -148,10 +148,11 @@ This tool is experimental and in the future it will be integrated in step cli. OPTIONS`) fmt.Fprintln(os.Stderr) flag.PrintDefaults() - fmt.Fprintln(os.Stderr, ` + fmt.Fprintf(os.Stderr, ` COPYRIGHT - (c) 2018-2020 Smallstep Labs, Inc.`) + (c) 2018-%d Smallstep Labs, Inc. +`, time.Now().Year()) os.Exit(1) } diff --git a/docs/provisioners.md b/docs/provisioners.md index 18010f88..d45e7865 100644 --- a/docs/provisioners.md +++ b/docs/provisioners.md @@ -346,6 +346,7 @@ Below is an example of an ACME provisioner in the `ca.json`: "type": "ACME", "name": "my-acme-provisioner", "forceCN": true, + "requireEAB": false, "claims": { "maxTLSCertDuration": "8h", "defaultTLSCertDuration": "2h", @@ -361,6 +362,9 @@ Below is an example of an ACME provisioner in the `ca.json`: * `forceCN` (optional): force one of the SANs to become the Common Name, if a common name is not provided. +* `requireEAB` (optional): require clients to provide External Account Binding + credentials when creating an ACME Account. + * `claims` (optional): overwrites the default claims set in the authority, see the [top](#provisioners) section for all the options. diff --git a/errs/error.go b/errs/error.go index 2c1fe6a9..60da9e1f 100644 --- a/errs/error.go +++ b/errs/error.go @@ -142,7 +142,7 @@ func (e *Error) UnmarshalJSON(data []byte) error { return err } e.Status = er.Status - e.Err = fmt.Errorf(er.Message) + e.Err = fmt.Errorf("%s", er.Message) return nil } diff --git a/errs/errors_test.go b/errs/errors_test.go index 58b95437..a2accebb 100644 --- a/errs/errors_test.go +++ b/errs/errors_test.go @@ -58,7 +58,7 @@ func TestError_UnmarshalJSON(t *testing.T) { t.Errorf("Error.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) } if !reflect.DeepEqual(tt.expected, e) { - t.Errorf("Error.UnmarshalJSON() wants = %v, got %v", tt.expected, e) + t.Errorf("Error.UnmarshalJSON() wants = %+v, got %+v", tt.expected, e) } }) } diff --git a/examples/ansible/smallstep-certs/defaults/main.yml b/examples/ansible/smallstep-certs/defaults/main.yml new file mode 100644 index 00000000..b4de90c5 --- /dev/null +++ b/examples/ansible/smallstep-certs/defaults/main.yml @@ -0,0 +1,18 @@ + + + +# Root cert for each will be saved in /etc/ssl/smallstep/ca/{{ ca_name }}/certs/root_ca.crt +smallstep_root_certs: [] +# - +# ca_name: your_ca +# ca_url: "https://certs.your_ca.ca.smallstep.com" +# ca_fingerprint: "56092...2200" + +# Each leaf cert will be saved in /etc/ssl/smallstep/leaf/{{ cert_subject }}/{{ cert_subject }}.crt|key +smallstep_leaf_certs: [] +# - +# ca_name: your_ca +# cert_subject: "{{ inventory_hostname }}" +# provisioner_name: "admin" +# provisioner_password: "{{ smallstep_ssh_provisioner_password }}" + diff --git a/examples/ansible/smallstep-certs/tasks/main.yml b/examples/ansible/smallstep-certs/tasks/main.yml new file mode 100644 index 00000000..a80a72a1 --- /dev/null +++ b/examples/ansible/smallstep-certs/tasks/main.yml @@ -0,0 +1,44 @@ + +- name: "Ensure provisioners directories exist" + file: + path: "/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}" + state: directory + mode: 0600 + owner: root + group: root + with_items: "{{ smallstep_leaf_certs }}" + no_log: true + +- name: "Ensure provisioner passwords are up to date" + copy: + dest: "/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt" + content: "{{ item.provisioner_password }}" + mode: 0700 + owner: root + group: root + with_items: "{{ smallstep_leaf_certs }}" + no_log: true + +- name: "Get root certs for CAs" + command: + cmd: "step ca bootstrap --context {{ item.context }} --ca-url {{ item.ca_url }} --fingerprint {{ item.ca_fingerprint }}" + with_items: "{{ smallstep_root_certs }}" + no_log: true + +- name: "Get leaf certs" + command: + cmd: "step ca certificate --context {{ item.context }} {{ item.cert_subject }} {{ item.cert_path }} {{ item.key_path }} --force --console --provisioner {{ item.provisioner_name }} --provisioner-password-file /etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt" + with_items: "{{ smallstep_leaf_certs }}" + no_log: true + +- name: Ensure cron to renew leaf certs is up to date + cron: + user: "root" + name: "renew leaf cert {{ item.cert_subject }}" + cron_file: smallstep + job: "step ca renew --context {{ item.context }} {{ item.cert_path }} {{ item.key_path }} --expires-in 6h --force >> /var/log/smallstep-{{ item.cert_subject }}.log 2>&1" + state: present + minute: "*/30" + with_items: "{{ smallstep_leaf_certs }}" + when: "{{ item.cron_renew }}" + no_log: true diff --git a/examples/ansible/smallstep-install/defaults/main.yml b/examples/ansible/smallstep-install/defaults/main.yml new file mode 100644 index 00000000..b3b5f067 --- /dev/null +++ b/examples/ansible/smallstep-install/defaults/main.yml @@ -0,0 +1,2 @@ +smallstep_install_step_version: 0.15.3 +smallstep_install_step_ssh_version: 0.19.1-1 diff --git a/examples/ansible/smallstep-install/tasks/main.yml b/examples/ansible/smallstep-install/tasks/main.yml new file mode 100644 index 00000000..083a7333 --- /dev/null +++ b/examples/ansible/smallstep-install/tasks/main.yml @@ -0,0 +1,29 @@ + +# These steps automate the installation guide here: +# https://smallstep.com/docs/sso-ssh/hosts/ + +- name: Download step binary + get_url: + url: "https://files.smallstep.com/step-linux-{{ smallstep_install_step_version }}" + dest: "/usr/local/bin/step-{{ smallstep_install_step_version }}" + mode: '0755' + +- name: Link binaries to correct version + file: + src: "/usr/local/bin/step-{{ smallstep_install_step_version }}" + dest: "{{ item }}" + state: link + with_items: + - /usr/bin/step + - /usr/local/bin/step + +- name: Link /usr/local/bin/step to correct binary version + file: + src: "/usr/local/bin/step-{{ smallstep_install_step_version }}" + dest: /usr/local/bin/step + state: link + +- name: Ensure step-ssh is installed + apt: + deb: "https://files.smallstep.com/step-ssh_{{ smallstep_install_step_ssh_version }}_amd64.deb" + state: present diff --git a/examples/ansible/smallstep-ssh/defaults/main.yml b/examples/ansible/smallstep-ssh/defaults/main.yml new file mode 100644 index 00000000..ae358948 --- /dev/null +++ b/examples/ansible/smallstep-ssh/defaults/main.yml @@ -0,0 +1,8 @@ +# If this host is behind a bastion this variable should contain the hostname of the bastion +smallstep_ssh_host_behind_bastion_name: "" +smallstep_ssh_host_is_bastion: false +smallstep_ssh_ca_url: "https://ssh.mycompany.ca.smallstep.com" +smallstep_ssh_ca_fingerprint: "XXXXXXXXXXXXXXX" + +# Whether or not to reinitialize the host even if it's already been installed +smallstep_ssh_force_reinit: true diff --git a/examples/ansible/smallstep-ssh/tasks/main.yml b/examples/ansible/smallstep-ssh/tasks/main.yml new file mode 100644 index 00000000..e3389663 --- /dev/null +++ b/examples/ansible/smallstep-ssh/tasks/main.yml @@ -0,0 +1,41 @@ + +# These steps automate the installation guide here: +# https://smallstep.com/docs/sso-ssh/hosts/ + +# TODO: Figure out how to make this idempotent instead of reinstalling on each run + +- name: Bootstrap node to connect to CA + command: "step ca bootstrap --context ssh --ca-url {{ smallstep_ssh_ca_url }} --fingerprint {{ smallstep_ssh_ca_fingerprint }} --force" +# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit + +- name: Get a host SSH certificate + command: "step ssh certificate --context ssh {{ inventory_hostname }} /etc/ssh/ssh_host_ecdsa_key.pub --host --sign --provisioner=\"Service Account\" --token=\"{{ smallstep_ssh_enrollment_token }}\" --force" +# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit + +- name: Configure SSHD (will be overwriten by the sshd template in Ansible later) + command: "step ssh config --context ssh --host --set Certificate=ssh_host_ecdsa_key-cert.pub --set Key=ssh_host_ecdsa_key" +# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit + +- name: Activate SmallStep PAM/NSS modules and nohup sshd + command: "step-ssh activate {{ inventory_hostname }}" +# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit + +- name: Generate host tags list + set_fact: + smallstep_ssh_host_tags_string: "{{ smallstep_ssh_host_tags | to_json | regex_replace('\\:\\ ','=') | regex_replace('\\{\\\"|,\\ \\\"', ' --tag \"') | regex_replace('[\\[\\]{}]') }}" + +- name: Generate command to register + set_fact: + smallstep_ssh_register_string: | + step-ssh-ctl register + --hostname {{ inventory_hostname }} + {% if not smallstep_ssh_host_is_bastion %}--bastion '{{ smallstep_ssh_host_behind_bastion_name|default("") }}'{% endif %} + {% if smallstep_ssh_host_is_bastion %}--is-bastion{% endif %} + {{ smallstep_ssh_host_tags_string }} + +- debug: var=smallstep_ssh_register_string + +- name: Register host with smallstep + command: "{{ smallstep_ssh_register_string }}" +# when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit + diff --git a/go.mod b/go.mod index 5307e95b..8f24e688 100644 --- a/go.mod +++ b/go.mod @@ -37,13 +37,14 @@ require ( github.com/urfave/cli v1.22.4 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.0 - go.step.sm/crypto v0.14.0 - go.step.sm/linkedca v0.9.0 + go.step.sm/crypto v0.15.0 + go.step.sm/linkedca v0.9.2 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f + golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect google.golang.org/api v0.47.0 - google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 - google.golang.org/grpc v1.41.0 + google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 + google.golang.org/grpc v1.43.0 google.golang.org/protobuf v1.27.1 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index d42da957..b4b0c75d 100644 --- a/go.sum +++ b/go.sum @@ -25,23 +25,18 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -72,13 +67,9 @@ github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -91,46 +82,35 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -141,37 +121,28 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0= github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go-v2 v0.18.0 h1:qZ+woO4SamnH/eEbjM2IDLhRNwIwND/RQyVlBLp3Jqg= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -179,34 +150,24 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158 h1:CevA8fI91PAnP8vpnXuB8ZYAZ5wqY86nAbxfgK8tWO4= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= @@ -214,9 +175,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -229,7 +188,6 @@ github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70d github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -240,13 +198,9 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -256,48 +210,35 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.1.10 h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -313,14 +254,11 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -360,7 +298,6 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -375,15 +312,11 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -396,9 +329,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22 h1:ub2sxhs2A0HRa2dWHavvmWxiVGXNfE9wI+gcTMwED8A= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -408,29 +339,19 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda h1:5ikpG9mYCMFiZX0nkxoV6aU2IpCHPdws3gCNgdZeEV0= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.3.0 h1:UOxjlb4xVNF93jak1mzzoBatyFju9nrkxpVwIp/QqxQ= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -446,9 +367,7 @@ github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= -github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -461,22 +380,18 @@ github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/password v0.1.1 h1:6JzmBqXprakgFEHwBgdchsjaA9x3GyjdI568bXKxa60= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= -github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1 h1:Yc026VyMyIpq1UWRnakHRG01U8fJm+nEfEmjoAb00n8= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -484,7 +399,6 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -492,13 +406,9 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= @@ -509,84 +419,60 @@ github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZdu github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/hudl/fargo v1.3.0 h1:0U6+BtN6LhaYuTnIJq4Wyq5cpn6O2kWrxAtcqBmYY6w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= -github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= -github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= @@ -603,20 +489,16 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU= github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -627,11 +509,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -642,66 +521,43 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f h1:8dM0ilqKL0Uzl42GABzzC4Oqlc3kGRILz0vgoff7nwg= github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f/go.mod h1:nwPd6pDNId/Xi16qtKrFHrauSwMNuvk+zcjk89wrnlA= github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU= github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -711,11 +567,9 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -723,14 +577,12 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -738,7 +590,6 @@ github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt2 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -746,15 +597,11 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -763,13 +610,10 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -780,7 +624,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/slackhq/nebula v1.5.2 h1:wuIOHsOnrNw3rQx8yPxXiGu8wAtAxxtUI/K8W7Vj7EI= github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIpR52WlE= @@ -789,39 +632,27 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/nosql v0.3.9 h1:YPy5PR3PXClqmpFaVv0wfXDXDc7NXGBE1auyU2c87dc= github.com/smallstep/nosql v0.3.9/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= -github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a h1:AhmOdSHeswKHBjhsLs/7+1voOxT+LLrSk/Nxvk35fug= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= @@ -835,37 +666,28 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= @@ -880,26 +702,22 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/crypto v0.14.0 h1:HzSkUDwqKhODKpsTxevJz956U2xVDZ3sDdGQVwR6Ttw= -go.step.sm/crypto v0.14.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= -go.step.sm/linkedca v0.9.0 h1:xKXZoRXy4B7LeGBZozq62IQ0p3v8dT33O9UOMpVtRtI= -go.step.sm/linkedca v0.9.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= +go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0= +go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= +go.step.sm/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok= +go.step.sm/linkedca v0.9.2/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -929,10 +747,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -945,10 +761,8 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -958,7 +772,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1009,8 +822,9 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1034,7 +848,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1108,8 +921,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1186,17 +1000,13 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard/windows v0.5.1 h1:OnYw96PF+CsIMrqWo5QP3Q59q5hY1rFErk/yN3cS+JQ= golang.zx2c4.com/wireguard/windows v0.5.1/go.mod h1:EApyTk/ZNrkbZjurHL1nleDYnsPpJYBO7LZEBCyDAHk= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -1275,8 +1085,8 @@ google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 h1:7yQQsvnwjfEahbNNEKcBHv3mR+HnB1ctGY/z1JXzx8M= -google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1305,10 +1115,10 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= +google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1324,29 +1134,20 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1367,15 +1168,9 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/pki/pki.go b/pki/pki.go index a8c298c7..998afb74 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -978,6 +978,7 @@ func (p *PKI) Save(opt ...ConfigOption) error { } if cfg.DB != nil { + os.MkdirAll(cfg.DB.DataSource, 0700) ui.PrintSelected("Database folder", cfg.DB.DataSource) } if cfg.Templates != nil { diff --git a/scep/api/api.go b/scep/api/api.go index 0c8c469b..4f8d897b 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -66,8 +66,8 @@ func New(scepAuth scep.Interface) api.RouterHandler { // Route traffic and implement the Router interface. func (h *Handler) Route(r api.Router) { getLink := h.Auth.GetLinkExplicit - r.MethodFunc(http.MethodGet, getLink("{provisionerID}", false, nil), h.lookupProvisioner(h.Get)) - r.MethodFunc(http.MethodPost, getLink("{provisionerID}", false, nil), h.lookupProvisioner(h.Post)) + r.MethodFunc(http.MethodGet, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Get)) + r.MethodFunc(http.MethodPost, getLink("{provisionerName}", false, nil), h.lookupProvisioner(h.Post)) } // Get handles all SCEP GET requests @@ -184,14 +184,14 @@ func decodeSCEPRequest(r *http.Request) (SCEPRequest, error) { func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { - name := chi.URLParam(r, "provisionerID") - provisionerID, err := url.PathUnescape(name) + name := chi.URLParam(r, "provisionerName") + provisionerName, err := url.PathUnescape(name) if err != nil { - api.WriteError(w, errors.Errorf("error url unescaping provisioner id '%s'", name)) + api.WriteError(w, errors.Errorf("error url unescaping provisioner name '%s'", name)) return } - p, err := h.Auth.LoadProvisionerByID("scep/" + provisionerID) + p, err := h.Auth.LoadProvisionerByName(provisionerName) if err != nil { api.WriteError(w, err) return @@ -212,7 +212,7 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { // GetCACert returns the CA certificates in a SCEP response func (h *Handler) GetCACert(ctx context.Context) (SCEPResponse, error) { - certs, err := h.Auth.GetCACertificates() + certs, err := h.Auth.GetCACertificates(ctx) if err != nil { return SCEPResponse{}, err } @@ -289,20 +289,29 @@ func (h *Handler) PKIOperation(ctx context.Context, request SCEPRequest) (SCEPRe // NOTE: at this point we have sufficient information for returning nicely signed CertReps csr := msg.CSRReqMessage.CSR - if msg.MessageType == microscep.PKCSReq { - + // NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication. + // The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems, + // even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless + // a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients. + // We'll have to see how it works out. + if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq { challengeMatches, err := h.Auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword) if err != nil { return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password")) } - if !challengeMatches { // TODO: can this be returned safely to the client? In the end, if the password was correct, that gains a bit of info too. return h.createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("wrong password provided")) } } - // TODO: check if CN already exists, if renewal is allowed and if existing should be revoked; fail if not + // TODO: authorize renewal: we can authorize renewals with the challenge password (if reusable secrets are used). + // Renewals OPTIONALLY include the challenge if the existing cert is used as authentication, but client SHOULD omit the challenge. + // This means that for renewal requests we should check the certificate provided to be signed before by the CA. We could + // enforce use of the challenge if we want too. That way we could be more flexible in terms of authentication scheme (i.e. reusing + // tokens from other provisioners, calling a webhook, storing multiple secrets, allowing them to be multi-use, etc). + // Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification + // of the client cert is not. certRep, err := h.Auth.SignCSR(ctx, csr, msg) if err != nil { diff --git a/scep/authority.go b/scep/authority.go index 47d1d41c..269e3ae1 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -20,10 +20,10 @@ import ( // Interface is the SCEP authority interface. type Interface interface { - LoadProvisionerByID(string) (provisioner.Interface, error) + LoadProvisionerByName(string) (provisioner.Interface, error) GetLinkExplicit(provName string, absoluteLink bool, baseURL *url.URL, inputs ...string) string - GetCACertificates() ([]*x509.Certificate, error) + GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) CreateFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) @@ -36,6 +36,7 @@ type Authority struct { prefix string dns string intermediateCertificate *x509.Certificate + caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate service *Service signAuth SignAuthority } @@ -56,7 +57,7 @@ type AuthorityOptions struct { // SignAuthority is the interface for a signing authority type SignAuthority interface { Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) - LoadProvisionerByID(string) (provisioner.Interface, error) + LoadProvisionerByName(string) (provisioner.Interface, error) } // New returns a new Authority that implements the SCEP interface. @@ -72,6 +73,8 @@ func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) { // in its entirety to make this more interoperable with the rest of // step-ca, I think. if ops.Service != nil { + authority.caCerts = ops.Service.certificateChain + // TODO(hs): look into refactoring SCEP into using just caCerts everywhere, if it makes sense for more elaborate SCEP configuration. Keeping it like this for clarity (for now). authority.intermediateCertificate = ops.Service.certificateChain[0] authority.service = ops.Service } @@ -82,7 +85,7 @@ func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) { var ( // TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2 defaultCapabilities = []string{ - "Renewal", + "Renewal", // NOTE: removing this will result in macOS SCEP client stating the server doesn't support renewal, but it uses PKCSreq to do so. "SHA-1", "SHA-256", "AES", @@ -92,26 +95,21 @@ var ( } ) -// LoadProvisionerByID calls out to the SignAuthority interface to load a -// provisioner by ID. -func (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error) { - return a.signAuth.LoadProvisionerByID(id) +// LoadProvisionerByName calls out to the SignAuthority interface to load a +// provisioner by name. +func (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, error) { + return a.signAuth.LoadProvisionerByName(name) } // GetLinkExplicit returns the requested link from the directory. func (a *Authority) GetLinkExplicit(provName string, abs bool, baseURL *url.URL, inputs ...string) string { - // TODO: taken from ACME; move it to directory (if we need a directory in SCEP)? return a.getLinkExplicit(provName, abs, baseURL, inputs...) } // getLinkExplicit returns an absolute or partial path to the given resource and a base // URL dynamically obtained from the request for which the link is being calculated. func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *url.URL, inputs ...string) string { - - // TODO: do we need to provide a way to provide a different suffix? - // Like "/cgi-bin/pkiclient.exe"? Or would it be enough to have that as the name? link := "/" + provisionerName - if abs { // Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351 u := url.URL{} @@ -137,7 +135,7 @@ func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *u } // GetCACertificates returns the certificate (chain) for the CA -func (a *Authority) GetCACertificates() ([]*x509.Certificate, error) { +func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) { // TODO: this should return: the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root // Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73 @@ -153,14 +151,28 @@ func (a *Authority) GetCACertificates() ([]*x509.Certificate, error) { // Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in // https://tools.ietf.org/id/draft-nourse-scep-21.html. Will continue using the CA directly for now. // - // The certificate to use should probably depend on the (configured) Provisioner and may + // The certificate to use should probably depend on the (configured) provisioner and may // use a distinct certificate, apart from the intermediate. - if a.intermediateCertificate == nil { + p, err := provisionerFromContext(ctx) + if err != nil { + return nil, err + } + + if len(a.caCerts) == 0 { return nil, errors.New("no intermediate certificate available in SCEP authority") } - return []*x509.Certificate{a.intermediateCertificate}, nil + certs := []*x509.Certificate{} + certs = append(certs, a.caCerts[0]) + + // NOTE: we're adding the CA roots here, but they are (highly likely) different than what the RFC means. + // Clients are responsible to select the right cert(s) to use, though. + if p.ShouldIncludeRootInChain() && len(a.caCerts) > 1 { + certs = append(certs, a.caCerts[1]) + } + + return certs, nil } // DecryptPKIEnvelope decrypts an enveloped message @@ -211,8 +223,6 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err // SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials // returns a new PKIMessage with CertRep data -//func (msg *PKIMessage) SignCSR(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey, template *x509.Certificate) (*PKIMessage, error) { -//func (a *Authority) SignCSR(ctx context.Context, msg *PKIMessage, template *x509.Certificate) (*PKIMessage, error) { func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) { // TODO: intermediate storage of the request? In SCEP it's possible to request a csr/certificate @@ -220,7 +230,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m // poll for the status. It seems to be similar as what can happen in ACME, so might want to model // the implementation after the one in the ACME authority. Requires storage, etc. - p, err := ProvisionerFromContext(ctx) + p, err := provisionerFromContext(ctx) if err != nil { return nil, err } @@ -267,11 +277,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m return nil, errors.Wrap(err, "error retrieving authorization options from SCEP provisioner") } - opts := provisioner.SignOptions{ - // NotBefore: provisioner.NewTimeDuration(o.NotBefore), - // NotAfter: provisioner.NewTimeDuration(o.NotAfter), - } - + opts := provisioner.SignOptions{} templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) if err != nil { return nil, errors.Wrap(err, "error creating template options from SCEP provisioner") @@ -292,10 +298,17 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m return nil, err } + // apparently the pkcs7 library uses a global default setting for the content encryption + // algorithm to use when en- or decrypting data. We need to restore the current setting after + // the cryptographic operation, so that other usages of the library are not influenced by + // this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses. + encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm + pkcs7.ContentEncryptionAlgorithm = p.GetContentEncryptionAlgorithm() e7, err := pkcs7.Encrypt(deg, msg.P7.Certificates) if err != nil { return nil, err } + pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore // PKIMessageAttributes to be signed config := pkcs7.SignerInfoConfig{ @@ -434,7 +447,7 @@ func (a *Authority) CreateFailureResponse(ctx context.Context, csr *x509.Certifi // MatchChallengePassword verifies a SCEP challenge password func (a *Authority) MatchChallengePassword(ctx context.Context, password string) (bool, error) { - p, err := ProvisionerFromContext(ctx) + p, err := provisionerFromContext(ctx) if err != nil { return false, err } @@ -453,7 +466,7 @@ func (a *Authority) MatchChallengePassword(ctx context.Context, password string) // GetCACaps returns the CA capabilities func (a *Authority) GetCACaps(ctx context.Context) []string { - p, err := ProvisionerFromContext(ctx) + p, err := provisionerFromContext(ctx) if err != nil { return defaultCapabilities } diff --git a/scep/common.go b/scep/common.go index ca87841f..73b16ed4 100644 --- a/scep/common.go +++ b/scep/common.go @@ -14,9 +14,9 @@ const ( ProvisionerContextKey = ContextKey("provisioner") ) -// ProvisionerFromContext searches the context for a SCEP provisioner. +// provisionerFromContext searches the context for a SCEP provisioner. // Returns the provisioner or an error. -func ProvisionerFromContext(ctx context.Context) (Provisioner, error) { +func provisionerFromContext(ctx context.Context) (Provisioner, error) { val := ctx.Value(ProvisionerContextKey) if val == nil { return nil, errors.New("provisioner expected in request context") diff --git a/scep/provisioner.go b/scep/provisioner.go index e1b7f8b1..679c6353 100644 --- a/scep/provisioner.go +++ b/scep/provisioner.go @@ -16,4 +16,6 @@ type Provisioner interface { GetOptions() *provisioner.Options GetChallengePassword() string GetCapabilities() []string + ShouldIncludeRootInChain() bool + GetContentEncryptionAlgorithm() int }