diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7470ed3b..9b831a90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ jobs: ci: uses: smallstep/workflows/.github/workflows/goCI.yml@main with: - os-dependencies: "libpcsclite-dev" - run-gitleaks: true + only-latest-golang: false + os-dependencies: 'libpcsclite-dev' run-codeql: true - make-test: true # run `make test` instead of the default test workflow + test-command: 'V=1 make test' secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 621f6a91..37fe2c2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,38 +55,12 @@ jobs: prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} goreleaser: - name: Upload Assets To Github w/ goreleaser - runs-on: ubuntu-latest needs: create_release permissions: id-token: write contents: write - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.19 - check-latest: true - - name: Install cosign - uses: sigstore/cosign-installer@v2 - with: - cosign-release: 'v1.13.1' - - name: Get Release Date - id: release_date - run: | - RELEASE_DATE=$(date +"%y-%m-%d") - echo "RELEASE_DATE=${RELEASE_DATE}" >> ${GITHUB_ENV} - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3 - with: - version: 'latest' - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }} - RELEASE_DATE: ${{ env.RELEASE_DATE }} - COSIGN_EXPERIMENTAL: 1 + uses: smallstep/workflows/.github/workflows/goreleaser.yml@main + secrets: inherit build_upload_docker: name: Build & Upload Docker Images diff --git a/.goreleaser.yml b/.goreleaser.yml index 5bdc2cb4..e64ee4b5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -31,7 +31,7 @@ builds: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} archives: - - + - &ARCHIVE # Can be used to change the archive formats for specific GOOSs. # Most common use case is to archive as zip on Windows. # Default is empty. @@ -45,6 +45,11 @@ archives: - README.md - LICENSE allow_different_binary_count: true + - + << : *ARCHIVE + id: unversioned + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" + nfpms: # Configure nFPM for .deb and .rpm releases @@ -56,7 +61,7 @@ nfpms: # List file contents: dpkg -c dist/step_...deb # Package metadata: dpkg --info dist/step_....deb # - - + - &NFPM builds: - step-ca package_name: step-ca @@ -76,6 +81,10 @@ nfpms: contents: - src: debian/copyright dst: /usr/share/doc/step-ca/copyright + - + << : *NFPM + id: unversioned + file_name_template: "{{ .PackageName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" source: enabled: true @@ -190,39 +199,40 @@ release: # - glob: ./glob/**/to/**/file/**/* # - glob: ./glob/foo/to/bar/file/foobar/override_from_previous -scoop: - # Template for the url which is determined by the given Token (github or gitlab) - # Default for github is "https://github.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" - # Default for gitlab is "https://gitlab.com///uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}" - # Default for gitea is "https://gitea.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" - url_template: "http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}" +scoops: + - + ids: [ default ] + # Template for the url which is determined by the given Token (github or gitlab) + # Default for github is "https://github.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" + # Default for gitlab is "https://gitlab.com///uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}" + # Default for gitea is "https://gitea.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" + url_template: "http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + # Repository to push the app manifest to. + bucket: + owner: smallstep + name: scoop-bucket - # Repository to push the app manifest to. - bucket: - owner: smallstep - name: scoop-bucket + # Git author used to commit to the repository. + # Defaults are shown. + commit_author: + name: goreleaserbot + email: goreleaser@smallstep.com - # Git author used to commit to the repository. - # Defaults are shown. - commit_author: - name: goreleaserbot - email: goreleaser@smallstep.com + # The project name and current git tag are used in the format string. + commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}" - # The project name and current git tag are used in the format string. - commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}" + # Your app's homepage. + # Default is empty. + homepage: "https://smallstep.com/docs/step-ca" - # Your app's homepage. - # Default is empty. - homepage: "https://smallstep.com/docs/step-ca" + # Skip uploads for prerelease. + skip_upload: auto - # Skip uploads for prerelease. - skip_upload: auto + # Your app's description. + # Default is empty. + description: "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH." - # Your app's description. - # Default is empty. - description: "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH." - - # Your app's license - # Default is empty. - license: "Apache-2.0" + # Your app's license + # Default is empty. + license: "Apache-2.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c11473..5265f1d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Fixed + +- Improved authentication for ACME requests using kid and provisioner name + (smallstep/certificates#1386). + + ## [v0.24.2] - 2023-05-11 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35f75159..2c13828e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ sudo yum install pcsc-lite-devel To build `step-ca`, clone this repository and run the following: ```shell -make bootstrap && make build GOFLAGS="" +make bootstrap && make build GO_ENVS="CGO_ENABLED=1" ``` When the build is complete, you will find binaries in `bin/`. diff --git a/Makefile b/Makefile index 5d7995f4..630b54b9 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,23 @@ endif DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC') LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"' -GOFLAGS := CGO_ENABLED=0 + +# Always explicitly enable or disable cgo, +# so that go doesn't silently fall back on +# non-cgo when gcc is not found. +ifeq (,$(findstring CGO_ENABLED,$(GO_ENVS))) + ifneq ($(origin GOFLAGS),undefined) + # This section is for backward compatibility with + # + # $ make build GOFLAGS="" + # + # which is how we recommended building step-ca with cgo support + # until June 2023. + GO_ENVS := $(GO_ENVS) CGO_ENABLED=1 + else + GO_ENVS := $(GO_ENVS) CGO_ENABLED=0 + endif +endif download: $Q go mod download @@ -71,7 +87,7 @@ build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) $Q mkdir -p $(@D) - $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) + $Q $(GOOS_OVERRIDE) GOFLAGS="$(GOFLAGS)" $(GO_ENVS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) # Target to force a build of step-ca without running tests simple: build @@ -93,10 +109,10 @@ generate: test: testdefault testtpmsimulator combinecoverage testdefault: - $Q $(GOFLAGS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./... + $Q $(GO_ENVS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./... testtpmsimulator: - $Q CGO_ENALBED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme + $Q CGO_ENABLED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme testcgo: $Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./... @@ -109,7 +125,7 @@ combinecoverage: integrate: integration integration: bin/$(BINNAME) - $Q $(GOFLAGS) gotestsum -- -tags=integration ./integration/... + $Q $(GO_ENVS) gotestsum -- -tags=integration ./integration/... .PHONY: integrate integration diff --git a/README.md b/README.md index 9544e7cd..9b454f51 100644 --- a/README.md +++ b/README.md @@ -119,18 +119,12 @@ See our installation docs [here](https://smallstep.com/docs/step-ca/installation ## Documentation -Documentation can be found in a handful of different places: - -1. On the web at https://smallstep.com/docs/step-ca. - -2. On the command line with `step help ca xxx` where `xxx` is the subcommand -you are interested in. Ex: `step help ca provisioner list`. - -3. In your browser, by running `step help --http=:8080 ca` from the command line +* [Official documentation](https://smallstep.com/docs/step-ca) is on smallstep.com +* The `step` command reference is available via `step help`, +[on smallstep.com](https://smallstep.com/docs/step-cli/reference/), +or by running `step help --http=:8080` from the command line and visiting http://localhost:8080. -4. The [docs](./docs/README.md) folder is being deprecated, but it still has some documentation and tutorials. - ## Feedback? * Tell us what you like and don't like about managing your PKI - we're eager to help solve problems in this space. diff --git a/acme/account.go b/acme/account.go index fa4b1167..38cca218 100644 --- a/acme/account.go +++ b/acme/account.go @@ -20,6 +20,16 @@ type Account struct { Status Status `json:"status"` OrdersURL string `json:"orders"` ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"` + LocationPrefix string `json:"-"` + ProvisionerName string `json:"-"` +} + +// GetLocation returns the URL location of the given account. +func (a *Account) GetLocation() string { + if a.LocationPrefix == "" { + return "" + } + return a.LocationPrefix + a.ID } // ToLog enables response logging. @@ -72,6 +82,7 @@ func (p *Policy) GetAllowedNameOptions() *policy.X509NameOptions { IPRanges: p.X509.Allowed.IPRanges, } } + func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions { if p == nil { return nil diff --git a/acme/account_test.go b/acme/account_test.go index b8ce7276..d4122500 100644 --- a/acme/account_test.go +++ b/acme/account_test.go @@ -66,6 +66,23 @@ func TestKeyToID(t *testing.T) { } } +func TestAccount_GetLocation(t *testing.T) { + locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/" + type test struct { + acc *Account + exp string + } + tests := map[string]test{ + "empty": {acc: &Account{LocationPrefix: ""}, exp: ""}, + "not-empty": {acc: &Account{ID: "bar", LocationPrefix: locationPrefix}, exp: locationPrefix + "bar"}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equals(t, tc.acc.GetLocation(), tc.exp) + }) + } +} + func TestAccount_IsValid(t *testing.T) { type test struct { acc *Account diff --git a/acme/api/account.go b/acme/api/account.go index 954cb9de..ce8b5799 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "errors" "net/http" @@ -67,6 +68,12 @@ func (u *UpdateAccountRequest) Validate() error { } } +// getAccountLocationPath returns the current account URL location. +// Returned location will be of the form: https:///acme//account/ +func getAccountLocationPath(ctx context.Context, linker acme.Linker, accID string) string { + return linker.GetLink(ctx, acme.AccountLinkType, accID) +} + // NewAccount is the handler resource for creating new ACME accounts. func NewAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -125,9 +132,11 @@ func NewAccount(w http.ResponseWriter, r *http.Request) { } acc = &acme.Account{ - Key: jwk, - Contact: nar.Contact, - Status: acme.StatusValid, + Key: jwk, + Contact: nar.Contact, + Status: acme.StatusValid, + LocationPrefix: getAccountLocationPath(ctx, linker, ""), + ProvisionerName: prov.GetName(), } if err := db.CreateAccount(ctx, acc); err != nil { render.Error(w, acme.WrapErrorISE(err, "error creating account")) @@ -152,7 +161,7 @@ func NewAccount(w http.ResponseWriter, r *http.Request) { linker.LinkAccount(ctx, acc) - w.Header().Set("Location", linker.GetLink(r.Context(), acme.AccountLinkType, acc.ID)) + w.Header().Set("Location", getAccountLocationPath(ctx, linker, acc.ID)) render.JSONStatus(w, acc, httpStatus) } diff --git a/acme/api/middleware.go b/acme/api/middleware.go index 5dcb93e3..ab2ab908 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "path" "strings" "go.step.sm/crypto/jose" @@ -16,7 +17,6 @@ import ( "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/logging" - "github.com/smallstep/nosql" ) type nextHTTP = func(http.ResponseWriter, *http.Request) @@ -293,7 +293,6 @@ func lookupJWK(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() db := acme.MustDatabaseFromContext(ctx) - linker := acme.MustLinkerFromContext(ctx) jws, err := jwsFromContext(ctx) if err != nil { @@ -301,19 +300,16 @@ func lookupJWK(next nextHTTP) nextHTTP { return } - kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "") kid := jws.Signatures[0].Protected.KeyID - if !strings.HasPrefix(kid, kidPrefix) { - render.Error(w, acme.NewError(acme.ErrorMalformedType, - "kid does not have required prefix; expected %s, but got %s", - kidPrefix, kid)) + if kid == "" { + render.Error(w, acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'")) return } - accID := strings.TrimPrefix(kid, kidPrefix) + accID := path.Base(kid) acc, err := db.GetAccount(ctx, accID) switch { - case nosql.IsErrNotFound(err): + case acme.IsErrNotFound(err): render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID)) return case err != nil: @@ -324,6 +320,45 @@ func lookupJWK(next nextHTTP) nextHTTP { render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active")) return } + + if storedLocation := acc.GetLocation(); storedLocation != "" { + if kid != storedLocation { + // ACME accounts should have a stored location equivalent to the + // kid in the ACME request. + render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, + "kid does not match stored account location; expected %s, but got %s", + storedLocation, kid)) + return + } + + // Verify that the provisioner with which the account was created + // matches the provisioner in the request URL. + reqProv := acme.MustProvisionerFromContext(ctx) + reqProvName := reqProv.GetName() + accProvName := acc.ProvisionerName + if reqProvName != accProvName { + // Provisioner in the URL must match the provisioner with + // which the account was created. + render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, + "account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s", + accProvName, reqProvName)) + return + } + } else { + // This code will only execute for old ACME accounts that do + // not have a cached location. The following validation was + // the original implementation of the `kid` check which has + // since been deprecated. However, the code will remain to + // ensure consistent behavior for old ACME accounts. + linker := acme.MustLinkerFromContext(ctx) + kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "") + if !strings.HasPrefix(kid, kidPrefix) { + render.Error(w, acme.NewError(acme.ErrorMalformedType, + "kid does not have required prefix; expected %s, but got %s", + kidPrefix, kid)) + return + } + } ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, jwkContextKey, acc.Key) next(w, r.WithContext(ctx)) diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index 6e9587f5..f7db647b 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -17,7 +17,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" - "github.com/smallstep/nosql/database" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" ) @@ -678,31 +677,7 @@ func TestHandler_lookupJWK(t *testing.T) { linker: acme.NewLinker("test.ca.smallstep.com", "acme"), ctx: ctx, statusCode: 400, - err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got ", prefix), - } - }, - "fail/bad-kid-prefix": func(t *testing.T) test { - _so := new(jose.SignerOptions) - _so.WithHeader("kid", "foo") - _signer, err := jose.NewSigner(jose.SigningKey{ - Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), - Key: jwk.Key, - }, _so) - assert.FatalError(t, err) - _jws, err := _signer.Sign([]byte("baz")) - assert.FatalError(t, err) - _raw, err := _jws.CompactSerialize() - assert.FatalError(t, err) - _parsed, err := jose.ParseJWS(_raw) - assert.FatalError(t, err) - ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, jwsContextKey, _parsed) - return test{ - db: &acme.MockDB{}, - linker: acme.NewLinker("test.ca.smallstep.com", "acme"), - ctx: ctx, - statusCode: 400, - err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got foo", prefix), + err: acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'"), } }, "fail/account-not-found": func(t *testing.T) test { @@ -713,7 +688,7 @@ func TestHandler_lookupJWK(t *testing.T) { db: &acme.MockDB{ MockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) { assert.Equals(t, accID, accID) - return nil, database.ErrNotFound + return nil, acme.ErrNotFound }, }, ctx: ctx, @@ -754,7 +729,77 @@ func TestHandler_lookupJWK(t *testing.T) { err: acme.NewError(acme.ErrorUnauthorizedType, "account is not active"), } }, - "ok": func(t *testing.T) test { + "fail/account-with-location-prefix/bad-kid": func(t *testing.T) test { + acc := &acme.Account{LocationPrefix: "foobar", Status: "valid"} + ctx := acme.NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), + db: &acme.MockDB{ + MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) { + assert.Equals(t, id, accID) + return acc, nil + }, + }, + ctx: ctx, + statusCode: http.StatusUnauthorized, + err: acme.NewError(acme.ErrorUnauthorizedType, "kid does not match stored account location; expected foobar, but %q", prefix+accID), + } + }, + "fail/account-with-location-prefix/bad-provisioner": func(t *testing.T) test { + acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerName: "other"} + ctx := acme.NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), + db: &acme.MockDB{ + MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) { + assert.Equals(t, id, accID) + return acc, nil + }, + }, + ctx: ctx, + next: func(w http.ResponseWriter, r *http.Request) { + _acc, err := accountFromContext(r.Context()) + assert.FatalError(t, err) + assert.Equals(t, _acc, acc) + _jwk, err := jwkFromContext(r.Context()) + assert.FatalError(t, err) + assert.Equals(t, _jwk, jwk) + w.Write(testBody) + }, + statusCode: http.StatusUnauthorized, + err: acme.NewError(acme.ErrorUnauthorizedType, + "account provisioner does not match requested provisioner; account provisioner = %s, reqested provisioner = %s", + prov.GetName(), "other"), + } + }, + "ok/account-with-location-prefix": func(t *testing.T) test { + acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerName: prov.GetName()} + ctx := acme.NewProvisionerContext(context.Background(), prov) + ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) + return test{ + linker: acme.NewLinker("test.ca.smallstep.com", "acme"), + db: &acme.MockDB{ + MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) { + assert.Equals(t, id, accID) + return acc, nil + }, + }, + ctx: ctx, + next: func(w http.ResponseWriter, r *http.Request) { + _acc, err := accountFromContext(r.Context()) + assert.FatalError(t, err) + assert.Equals(t, _acc, acc) + _jwk, err := jwkFromContext(r.Context()) + assert.FatalError(t, err) + assert.Equals(t, _jwk, jwk) + w.Write(testBody) + }, + statusCode: http.StatusOK, + } + }, + "ok/account-without-location-prefix": func(t *testing.T) test { acc := &acme.Account{Status: "valid", Key: jwk} ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) diff --git a/acme/challenge_tpmsimulator_test.go b/acme/challenge_tpmsimulator_test.go index dc427028..cb893b14 100644 --- a/acme/challenge_tpmsimulator_test.go +++ b/acme/challenge_tpmsimulator_test.go @@ -49,8 +49,9 @@ func withSimulator(t *testing.T) tpm.NewTPMOption { err := sim.Close() require.NoError(t, err) }) - sim = simulator.New() - err := sim.Open() + sim, err := simulator.New() + require.NoError(t, err) + err = sim.Open() require.NoError(t, err) return tpm.WithSimulator(sim) } diff --git a/acme/db.go b/acme/db.go index d7c9d5f4..fa9aa0de 100644 --- a/acme/db.go +++ b/acme/db.go @@ -12,6 +12,12 @@ import ( // account. var ErrNotFound = errors.New("not found") +// IsErrNotFound returns true if the error is a "not found" error. Returns false +// otherwise. +func IsErrNotFound(err error) bool { + return errors.Is(err, ErrNotFound) +} + // DB is the DB interface expected by the step-ca ACME API. type DB interface { CreateAccount(ctx context.Context, acc *Account) error diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 8067a4b9..d590ccb3 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -13,12 +13,14 @@ import ( // dbAccount represents an ACME account. type dbAccount struct { - ID string `json:"id"` - Key *jose.JSONWebKey `json:"key"` - Contact []string `json:"contact,omitempty"` - Status acme.Status `json:"status"` - CreatedAt time.Time `json:"createdAt"` - DeactivatedAt time.Time `json:"deactivatedAt"` + ID string `json:"id"` + Key *jose.JSONWebKey `json:"key"` + Contact []string `json:"contact,omitempty"` + Status acme.Status `json:"status"` + LocationPrefix string `json:"locationPrefix"` + ProvisionerName string `json:"provisionerName"` + CreatedAt time.Time `json:"createdAt"` + DeactivatedAt time.Time `json:"deactivatedAt"` } func (dba *dbAccount) clone() *dbAccount { @@ -62,10 +64,12 @@ func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) } return &acme.Account{ - Status: dbacc.Status, - Contact: dbacc.Contact, - Key: dbacc.Key, - ID: dbacc.ID, + Status: dbacc.Status, + Contact: dbacc.Contact, + Key: dbacc.Key, + ID: dbacc.ID, + LocationPrefix: dbacc.LocationPrefix, + ProvisionerName: dbacc.ProvisionerName, }, nil } @@ -87,11 +91,13 @@ func (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error { } dba := &dbAccount{ - ID: acc.ID, - Key: acc.Key, - Contact: acc.Contact, - Status: acc.Status, - CreatedAt: clock.Now(), + ID: acc.ID, + Key: acc.Key, + Contact: acc.Contact, + Status: acc.Status, + CreatedAt: clock.Now(), + LocationPrefix: acc.LocationPrefix, + ProvisionerName: acc.ProvisionerName, } kid, err := acme.KeyToID(dba.Key) diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 6097cc5a..085ce2eb 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -197,6 +197,8 @@ func TestDB_getAccountIDByKeyID(t *testing.T) { func TestDB_GetAccount(t *testing.T) { accID := "accID" + locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/" + provisionerName := "foo" type test struct { db nosql.DB err error @@ -222,12 +224,14 @@ func TestDB_GetAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) dbacc := &dbAccount{ - ID: accID, - Status: acme.StatusDeactivated, - CreatedAt: now, - DeactivatedAt: now, - Contact: []string{"foo", "bar"}, - Key: jwk, + ID: accID, + Status: acme.StatusDeactivated, + CreatedAt: now, + DeactivatedAt: now, + Contact: []string{"foo", "bar"}, + Key: jwk, + LocationPrefix: locationPrefix, + ProvisionerName: provisionerName, } b, err := json.Marshal(dbacc) assert.FatalError(t, err) @@ -266,6 +270,8 @@ func TestDB_GetAccount(t *testing.T) { assert.Equals(t, acc.ID, tc.dbacc.ID) assert.Equals(t, acc.Status, tc.dbacc.Status) assert.Equals(t, acc.Contact, tc.dbacc.Contact) + assert.Equals(t, acc.LocationPrefix, tc.dbacc.LocationPrefix) + assert.Equals(t, acc.ProvisionerName, tc.dbacc.ProvisionerName) assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID) } }) @@ -379,6 +385,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) { } func TestDB_CreateAccount(t *testing.T) { + locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/" type test struct { db nosql.DB acc *acme.Account @@ -390,9 +397,10 @@ func TestDB_CreateAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) acc := &acme.Account{ - Status: acme.StatusValid, - Contact: []string{"foo", "bar"}, - Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + Key: jwk, + LocationPrefix: locationPrefix, } return test{ db: &db.MockNoSQLDB{ @@ -413,9 +421,10 @@ func TestDB_CreateAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) acc := &acme.Account{ - Status: acme.StatusValid, - Contact: []string{"foo", "bar"}, - Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + Key: jwk, + LocationPrefix: locationPrefix, } return test{ db: &db.MockNoSQLDB{ @@ -436,9 +445,10 @@ func TestDB_CreateAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) acc := &acme.Account{ - Status: acme.StatusValid, - Contact: []string{"foo", "bar"}, - Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + Key: jwk, + LocationPrefix: locationPrefix, } return test{ db: &db.MockNoSQLDB{ @@ -456,6 +466,8 @@ func TestDB_CreateAccount(t *testing.T) { assert.FatalError(t, json.Unmarshal(nu, dbacc)) assert.Equals(t, dbacc.ID, string(key)) assert.Equals(t, dbacc.Contact, acc.Contact) + assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix) + assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName) assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID) assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt)) assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt)) @@ -479,9 +491,10 @@ func TestDB_CreateAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) acc := &acme.Account{ - Status: acme.StatusValid, - Contact: []string{"foo", "bar"}, - Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + Key: jwk, + LocationPrefix: locationPrefix, } return test{ db: &db.MockNoSQLDB{ @@ -500,6 +513,8 @@ func TestDB_CreateAccount(t *testing.T) { assert.FatalError(t, json.Unmarshal(nu, dbacc)) assert.Equals(t, dbacc.ID, string(key)) assert.Equals(t, dbacc.Contact, acc.Contact) + assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix) + assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName) assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID) assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt)) assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt)) @@ -539,12 +554,14 @@ func TestDB_UpdateAccount(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) dbacc := &dbAccount{ - ID: accID, - Status: acme.StatusDeactivated, - CreatedAt: now, - DeactivatedAt: now, - Contact: []string{"foo", "bar"}, - Key: jwk, + ID: accID, + Status: acme.StatusDeactivated, + CreatedAt: now, + DeactivatedAt: now, + Contact: []string{"foo", "bar"}, + LocationPrefix: "foo", + ProvisionerName: "alpha", + Key: jwk, } b, err := json.Marshal(dbacc) assert.FatalError(t, err) @@ -644,10 +661,12 @@ func TestDB_UpdateAccount(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{ - ID: accID, - Status: acme.StatusDeactivated, - Contact: []string{"foo", "bar"}, - Key: jwk, + ID: accID, + Status: acme.StatusDeactivated, + Contact: []string{"baz", "zap"}, + LocationPrefix: "bar", + ProvisionerName: "beta", + Key: jwk, } return test{ acc: acc, @@ -666,7 +685,10 @@ func TestDB_UpdateAccount(t *testing.T) { assert.FatalError(t, json.Unmarshal(nu, dbNew)) assert.Equals(t, dbNew.ID, dbacc.ID) assert.Equals(t, dbNew.Status, acc.Status) - assert.Equals(t, dbNew.Contact, dbacc.Contact) + assert.Equals(t, dbNew.Contact, acc.Contact) + // LocationPrefix should not change. + assert.Equals(t, dbNew.LocationPrefix, dbacc.LocationPrefix) + assert.Equals(t, dbNew.ProvisionerName, dbacc.ProvisionerName) assert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID) assert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt) assert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now)) @@ -686,12 +708,7 @@ func TestDB_UpdateAccount(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, tc.acc.ID, dbacc.ID) - assert.Equals(t, tc.acc.Status, dbacc.Status) - assert.Equals(t, tc.acc.Contact, dbacc.Contact) - assert.Equals(t, tc.acc.Key.KeyID, dbacc.Key.KeyID) - } + assert.Nil(t, tc.err) } }) } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index b30292fd..11b18ebb 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -24,6 +24,7 @@ import ( "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/webhook" ) // awsIssuer is the string used as issuer in the generated tokens. @@ -521,7 +522,11 @@ func (p *AWS) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro commonNameValidator(payload.Claims.Subject), newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), - p.ctl.newWebhookController(data, linkedca.Webhook_X509), + p.ctl.newWebhookController( + data, + linkedca.Webhook_X509, + webhook.WithAuthorizationPrincipal(doc.InstanceID), + ), ), nil } @@ -804,6 +809,10 @@ func (p *AWS) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), // Call webhooks - p.ctl.newWebhookController(data, linkedca.Webhook_SSH), + p.ctl.newWebhookController( + data, + linkedca.Webhook_SSH, + webhook.WithAuthorizationPrincipal(doc.InstanceID), + ), ), nil } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index c88a098d..1c70a132 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -20,6 +20,7 @@ import ( "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/webhook" ) // azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens. @@ -403,7 +404,11 @@ func (p *Azure) AuthorizeSign(_ context.Context, token string) ([]SignOption, er defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), - p.ctl.newWebhookController(data, linkedca.Webhook_X509), + p.ctl.newWebhookController( + data, + linkedca.Webhook_X509, + webhook.WithAuthorizationPrincipal(identityObjectID), + ), ), nil } @@ -421,7 +426,7 @@ func (p *Azure) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, return nil, errs.Unauthorized("azure.AuthorizeSSHSign; sshCA is disabled for provisioner '%s'", p.GetName()) } - _, name, _, _, _, err := p.authorizeToken(token) + _, name, _, _, identityObjectID, err := p.authorizeToken(token) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign") } @@ -473,7 +478,11 @@ func (p *Azure) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), // Call webhooks - p.ctl.newWebhookController(data, linkedca.Webhook_SSH), + p.ctl.newWebhookController( + data, + linkedca.Webhook_SSH, + webhook.WithAuthorizationPrincipal(identityObjectID), + ), ), nil } diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index 25030fbc..09f6a6bb 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -4,12 +4,12 @@ import ( "context" "crypto/x509" "net/http" - "regexp" "strings" "time" "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/webhook" "go.step.sm/linkedca" "golang.org/x/crypto/ssh" ) @@ -77,7 +77,7 @@ func (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificat return DefaultAuthorizeSSHRenew(ctx, c, cert) } -func (c *Controller) newWebhookController(templateData WebhookSetter, certType linkedca.Webhook_CertType) *WebhookController { +func (c *Controller) newWebhookController(templateData WebhookSetter, certType linkedca.Webhook_CertType, opts ...webhook.RequestBodyOption) *WebhookController { client := c.webhookClient if client == nil { client = http.DefaultClient @@ -87,6 +87,7 @@ func (c *Controller) newWebhookController(templateData WebhookSetter, certType l client: client, webhooks: c.webhooks, certType: certType, + options: opts, } } @@ -115,20 +116,18 @@ func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identit switch k := p.(type) { case *OIDC: // OIDC principals would be: - // ~~1. Preferred usernames.~~ Note: Under discussion, currently disabled - // 2. Sanitized local. - // 3. Raw local (if different). - // 4. Email address. + // ~~1. Preferred usernames.~~ Note: Under discussion, currently disabled + // 2. Sanitized local. + // 3. Raw local (if different). + // 4. Email address. name := SanitizeSSHUserPrincipal(email) - if !sshUserRegex.MatchString(name) { - return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email) - } usernames := []string{name} if i := strings.LastIndex(email, "@"); i >= 0 { usernames = append(usernames, email[:i]) } usernames = append(usernames, email) return &Identity{ + // Remove duplicated and empty usernames. Usernames: SanitizeStringSlices(usernames), }, nil default: @@ -178,8 +177,6 @@ func DefaultAuthorizeSSHRenew(_ context.Context, p *Controller, cert *ssh.Certif return nil } -var sshUserRegex = regexp.MustCompile("^[a-z][-a-z0-9_]*$") - // SanitizeStringSlices removes duplicated an empty strings. func SanitizeStringSlices(original []string) []string { output := []string{} diff --git a/authority/provisioner/controller_test.go b/authority/provisioner/controller_test.go index c628f074..cddfb635 100644 --- a/authority/provisioner/controller_test.go +++ b/authority/provisioner/controller_test.go @@ -4,15 +4,18 @@ import ( "context" "crypto/x509" "fmt" + "net/http" "reflect" "testing" "time" + "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" "go.step.sm/linkedca" "golang.org/x/crypto/ssh" "github.com/smallstep/certificates/authority/policy" + "github.com/smallstep/certificates/webhook" ) var trueValue = true @@ -167,6 +170,12 @@ func TestController_GetIdentity(t *testing.T) { }}, args{ctx, "jane@doe.org"}, &Identity{ Usernames: []string{"jane"}, }, false}, + {"ok badname", fields{&OIDC{}, nil}, args{ctx, "1000@doe.org"}, &Identity{ + Usernames: []string{"1000", "1000@doe.org"}, + }, false}, + {"ok sanitized badname", fields{&OIDC{}, nil}, args{ctx, "1000+10@doe.org"}, &Identity{ + Usernames: []string{"1000_10", "1000+10", "1000+10@doe.org"}, + }, false}, {"fail provisioner", fields{&JWK{}, nil}, args{ctx, "jane@doe.org"}, nil, true}, {"fail custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) { return nil, fmt.Errorf("an error") @@ -449,16 +458,39 @@ func TestDefaultAuthorizeSSHRenew(t *testing.T) { } func Test_newWebhookController(t *testing.T) { - c := &Controller{} - data := x509util.TemplateData{"foo": "bar"} - ctl := c.newWebhookController(data, linkedca.Webhook_X509) - if !reflect.DeepEqual(ctl.TemplateData, data) { - t.Error("Failed to set templateData") + cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock()) + if err != nil { + t.Fatal(err) } - if ctl.certType != linkedca.Webhook_X509 { - t.Error("Failed to set certType") + opts := []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)} + + type args struct { + templateData WebhookSetter + certType linkedca.Webhook_CertType + opts []webhook.RequestBodyOption } - if ctl.client == nil { - t.Error("Failed to set client") + tests := []struct { + name string + args args + want *WebhookController + }{ + {"ok", args{x509util.TemplateData{"foo": "bar"}, linkedca.Webhook_X509, nil}, &WebhookController{ + TemplateData: x509util.TemplateData{"foo": "bar"}, + certType: linkedca.Webhook_X509, + client: http.DefaultClient, + }}, + {"ok with options", args{x509util.TemplateData{"foo": "bar"}, linkedca.Webhook_SSH, opts}, &WebhookController{ + TemplateData: x509util.TemplateData{"foo": "bar"}, + certType: linkedca.Webhook_SSH, + client: http.DefaultClient, + options: opts, + }}, + } + for _, tt := range tests { + c := &Controller{} + got := c.newWebhookController(tt.args.templateData, tt.args.certType, tt.args.opts...) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("newWebhookController() = %v, want %v", got, tt.want) + } } } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 2b5b932b..8634fecc 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -21,6 +21,7 @@ import ( "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/webhook" ) // gcpCertsURL is the url that serves Google OAuth2 public keys. @@ -275,7 +276,11 @@ func (p *GCP) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), - p.ctl.newWebhookController(data, linkedca.Webhook_X509), + p.ctl.newWebhookController( + data, + linkedca.Webhook_X509, + webhook.WithAuthorizationPrincipal(ce.InstanceID), + ), ), nil } @@ -442,6 +447,10 @@ func (p *GCP) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), // Call webhooks - p.ctl.newWebhookController(data, linkedca.Webhook_SSH), + p.ctl.newWebhookController( + data, + linkedca.Webhook_SSH, + webhook.WithAuthorizationPrincipal(ce.InstanceID), + ), ), nil } diff --git a/authority/provisioner/provisioner_test.go b/authority/provisioner/provisioner_test.go index 65fb8e1d..865e5291 100644 --- a/authority/provisioner/provisioner_test.go +++ b/authority/provisioner/provisioner_test.go @@ -76,13 +76,6 @@ func TestDefaultIdentityFunc(t *testing.T) { err: errors.New("provisioner type '*provisioner.X5C' not supported by identity function"), } }, - "fail/bad-ssh-regex": func(t *testing.T) test { - return test{ - p: &OIDC{}, - email: "$%^#_>@smallstep.com", - err: errors.New("invalid principal '______' from email '$%^#_>@smallstep.com'"), - } - }, "ok": func(t *testing.T) test { return test{ p: &OIDC{}, @@ -142,6 +135,13 @@ func TestDefaultIdentityFunc(t *testing.T) { identity: &Identity{Usernames: []string{"john", "john@smallstep.com"}}, } }, + "ok/badname": func(t *testing.T) test { + return test{ + p: &OIDC{}, + email: "$%^#_>@smallstep.com", + identity: &Identity{Usernames: []string{"______", "$%^#_>", "$%^#_>@smallstep.com"}}, + } + }, } for name, get := range tests { t.Run(name, func(t *testing.T) { diff --git a/authority/provisioner/webhook.go b/authority/provisioner/webhook.go index cb15547d..407b84d8 100644 --- a/authority/provisioner/webhook.go +++ b/authority/provisioner/webhook.go @@ -30,6 +30,7 @@ type WebhookController struct { client *http.Client webhooks []*Webhook certType linkedca.Webhook_CertType + options []webhook.RequestBodyOption TemplateData WebhookSetter } @@ -39,6 +40,14 @@ func (wc *WebhookController) Enrich(req *webhook.RequestBody) error { if wc == nil { return nil } + + // Apply extra options in the webhook controller + for _, fn := range wc.options { + if err := fn(req); err != nil { + return err + } + } + for _, wh := range wc.webhooks { if wh.Kind != linkedca.Webhook_ENRICHING.String() { continue @@ -63,6 +72,14 @@ func (wc *WebhookController) Authorize(req *webhook.RequestBody) error { if wc == nil { return nil } + + // Apply extra options in the webhook controller + for _, fn := range wc.options { + if err := fn(req); err != nil { + return err + } + } + for _, wh := range wc.webhooks { if wh.Kind != linkedca.Webhook_AUTHORIZING.String() { continue diff --git a/authority/provisioner/webhook_test.go b/authority/provisioner/webhook_test.go index a7895638..656d75d8 100644 --- a/authority/provisioner/webhook_test.go +++ b/authority/provisioner/webhook_test.go @@ -4,6 +4,7 @@ import ( "crypto/hmac" "crypto/sha256" "crypto/tls" + "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" @@ -16,6 +17,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/webhook" + "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" "go.step.sm/linkedca" ) @@ -96,12 +98,18 @@ func TestWebhookController_isCertTypeOK(t *testing.T) { } func TestWebhookController_Enrich(t *testing.T) { + cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock()) + if err != nil { + t.Fatal(err) + } + type test struct { ctl *WebhookController req *webhook.RequestBody responses []*webhook.ResponseBody expectErr bool expectTemplateData any + assertRequest func(t *testing.T, req *webhook.RequestBody) } tests := map[string]test{ "ok/no enriching webhooks": { @@ -170,6 +178,29 @@ func TestWebhookController_Enrich(t *testing.T) { }, }, }, + "ok/with options": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}}, + TemplateData: x509util.TemplateData{}, + options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)}, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{{Allow: true, Data: map[string]any{"role": "bar"}}}, + expectErr: false, + expectTemplateData: x509util.TemplateData{"Webhooks": map[string]any{"people": map[string]any{"role": "bar"}}}, + assertRequest: func(t *testing.T, req *webhook.RequestBody) { + key, err := x509.MarshalPKIXPublicKey(cert.PublicKey) + assert.FatalError(t, err) + assert.Equals(t, &webhook.X5CCertificate{ + Raw: cert.Raw, + PublicKey: key, + PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(), + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + }, req.X5CCertificate) + }, + }, "deny": { ctl: &WebhookController{ client: http.DefaultClient, @@ -181,6 +212,20 @@ func TestWebhookController_Enrich(t *testing.T) { expectErr: true, expectTemplateData: x509util.TemplateData{}, }, + "fail/with options": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}}, + TemplateData: x509util.TemplateData{}, + options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{ + PublicKey: []byte("bad"), + })}, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{{Allow: false}}, + expectErr: true, + expectTemplateData: x509util.TemplateData{}, + }, } for name, test := range tests { t.Run(name, func(t *testing.T) { @@ -200,16 +245,25 @@ func TestWebhookController_Enrich(t *testing.T) { t.Fatalf("Got err %v, want %v", err, test.expectErr) } assert.Equals(t, test.expectTemplateData, test.ctl.TemplateData) + if test.assertRequest != nil { + test.assertRequest(t, test.req) + } }) } } func TestWebhookController_Authorize(t *testing.T) { + cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock()) + if err != nil { + t.Fatal(err) + } + type test struct { - ctl *WebhookController - req *webhook.RequestBody - responses []*webhook.ResponseBody - expectErr bool + ctl *WebhookController + req *webhook.RequestBody + responses []*webhook.ResponseBody + expectErr bool + assertRequest func(t *testing.T, req *webhook.RequestBody) } tests := map[string]test{ "ok/no enriching webhooks": { @@ -240,6 +294,27 @@ func TestWebhookController_Authorize(t *testing.T) { responses: []*webhook.ResponseBody{{Allow: false}}, expectErr: false, }, + "ok/with options": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}}, + options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)}, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{{Allow: true}}, + expectErr: false, + assertRequest: func(t *testing.T, req *webhook.RequestBody) { + key, err := x509.MarshalPKIXPublicKey(cert.PublicKey) + assert.FatalError(t, err) + assert.Equals(t, &webhook.X5CCertificate{ + Raw: cert.Raw, + PublicKey: key, + PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(), + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + }, req.X5CCertificate) + }, + }, "deny": { ctl: &WebhookController{ client: http.DefaultClient, @@ -249,6 +324,18 @@ func TestWebhookController_Authorize(t *testing.T) { responses: []*webhook.ResponseBody{{Allow: false}}, expectErr: true, }, + "fail/with options": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}}, + options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{ + PublicKey: []byte("bad"), + })}, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{{Allow: false}}, + expectErr: true, + }, } for name, test := range tests { t.Run(name, func(t *testing.T) { @@ -267,6 +354,9 @@ func TestWebhookController_Authorize(t *testing.T) { if (err != nil) != test.expectErr { t.Fatalf("Got err %v, want %v", err, test.expectErr) } + if test.assertRequest != nil { + test.assertRequest(t, test.req) + } }) } } diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index d2a7c954..be606ae8 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -15,6 +15,7 @@ import ( "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/webhook" ) // x5cPayload extends jwt.Claims with step attributes. @@ -215,7 +216,8 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro // The X509 certificate will be available using the template variable // AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be // used to get all the domains. - data.SetAuthorizationCertificate(claims.chains[0][0]) + x5cLeaf := claims.chains[0][0] + data.SetAuthorizationCertificate(x5cLeaf) templateOptions, err := TemplateOptions(p.Options, data) if err != nil { @@ -238,7 +240,7 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro newProvisionerExtensionOption(TypeX5C, p.Name, ""), profileLimitDuration{ p.ctl.Claimer.DefaultTLSCertDuration(), - claims.chains[0][0].NotBefore, claims.chains[0][0].NotAfter, + x5cLeaf.NotBefore, x5cLeaf.NotAfter, }, // validators commonNameValidator(claims.Subject), @@ -246,7 +248,12 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), - p.ctl.newWebhookController(data, linkedca.Webhook_X509), + p.ctl.newWebhookController( + data, + linkedca.Webhook_X509, + webhook.WithX5CCertificate(x5cLeaf), + webhook.WithAuthorizationPrincipal(x5cLeaf.Subject.CommonName), + ), }, nil } @@ -305,7 +312,8 @@ func (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e // The X509 certificate will be available using the template variable // AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be // used to get all the domains. - data.SetAuthorizationCertificate(claims.chains[0][0]) + x5cLeaf := claims.chains[0][0] + data.SetAuthorizationCertificate(x5cLeaf) templateOptions, err := TemplateSSHOptions(p.Options, data) if err != nil { @@ -325,7 +333,7 @@ func (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e return append(signOptions, p, // Checks the validity bounds, and set the validity if has not been set. - &sshLimitDuration{p.ctl.Claimer, claims.chains[0][0].NotAfter}, + &sshLimitDuration{p.ctl.Claimer, x5cLeaf.NotAfter}, // Validate public key. &sshDefaultPublicKeyValidator{}, // Validate the validity period. @@ -335,6 +343,11 @@ func (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()), // Call webhooks - p.ctl.newWebhookController(data, linkedca.Webhook_SSH), + p.ctl.newWebhookController( + data, + linkedca.Webhook_SSH, + webhook.WithX5CCertificate(x5cLeaf), + webhook.WithAuthorizationPrincipal(x5cLeaf.Subject.CommonName), + ), ), nil } diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index 72f9f947..f9a2604b 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -12,6 +12,7 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/randutil" + "go.step.sm/linkedca" "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" @@ -497,6 +498,8 @@ func TestX5C_AuthorizeSign(t *testing.T) { assert.Equals(t, nil, v.policyEngine) case *WebhookController: assert.Len(t, 0, v.webhooks) + assert.Equals(t, linkedca.Webhook_X509, v.certType) + assert.Len(t, 2, v.options) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } @@ -801,6 +804,8 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc: case *WebhookController: assert.Len(t, 0, v.webhooks) + assert.Equals(t, linkedca.Webhook_SSH, v.certType) + assert.Len(t, 2, v.options) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 9477a53e..62c422d4 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -606,7 +606,13 @@ func doReload(ca *CA) error { } // Use same address in new server newCA.srv.Addr = ca.srv.Addr - return ca.srv.Reload(newCA.srv) + if err := ca.srv.Reload(newCA.srv); err != nil { + return err + } + + // Wait a few ms until the http server calls listener.Accept() + time.Sleep(100 * time.Millisecond) + return nil } func TestBootstrapListener(t *testing.T) { diff --git a/ca/tls_test.go b/ca/tls_test.go index 946a6cb5..24b8ef01 100644 --- a/ca/tls_test.go +++ b/ca/tls_test.go @@ -229,7 +229,7 @@ func TestClient_GetServerTLSConfig_http(t *testing.T) { defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { - t.Fatalf("ioutil.RealAdd() error = %v", err) + t.Fatalf("io.ReadAll() error = %v", err) } if !bytes.Equal(b, []byte("ok")) { t.Errorf("response body unexpected, got %s, want ok", b) @@ -343,7 +343,7 @@ func TestClient_GetServerTLSConfig_renew(t *testing.T) { defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { - t.Errorf("ioutil.RealAdd() error = %v", err) + t.Errorf("io.ReadAll() error = %v", err) return } if !bytes.Equal(b, []byte("ok")) { diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 8d3797f4..5908cb7d 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -37,6 +37,7 @@ type VaultOptions struct { PKIRoleEd25519 string `json:"pkiRoleEd25519,omitempty"` AuthType string `json:"authType,omitempty"` AuthMountPath string `json:"authMountPath,omitempty"` + Namespace string `json:"namespace,omitempty"` AuthOptions json.RawMessage `json:"authOptions,omitempty"` } @@ -90,6 +91,10 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { return nil, fmt.Errorf("unable to configure %s auth method: %w", vc.AuthType, err) } + if vc.Namespace != "" { + client.SetNamespace(vc.Namespace) + } + authInfo, err := client.Auth().Login(ctx, method) if err != nil { return nil, fmt.Errorf("unable to login to %s auth method: %w", vc.AuthType, err) diff --git a/docker/Dockerfile.hsm b/docker/Dockerfile.hsm index 8ae1e7c7..c5a54d8c 100644 --- a/docker/Dockerfile.hsm +++ b/docker/Dockerfile.hsm @@ -6,7 +6,7 @@ COPY . . RUN apt-get update RUN apt-get install -y --no-install-recommends \ gcc pkgconf libpcsclite-dev libcap2-bin -RUN make V=1 GOFLAGS="" bin/step-ca +RUN make V=1 GO_ENVS="CGO_ENABLED=1" bin/step-ca RUN setcap CAP_NET_BIND_SERVICE=+eip bin/step-ca FROM smallstep/step-kms-plugin:bullseye AS kms diff --git a/go.mod b/go.mod index 528067fc..58881ecf 100644 --- a/go.mod +++ b/go.mod @@ -3,46 +3,48 @@ module github.com/smallstep/certificates go 1.19 require ( - cloud.google.com/go/longrunning v0.4.2 - cloud.google.com/go/security v1.14.1 + cloud.google.com/go/longrunning v0.5.1 + cloud.google.com/go/security v1.15.1 github.com/Masterminds/sprig/v3 v3.2.3 + github.com/dgraph-io/badger v1.6.2 + github.com/dgraph-io/badger/v2 v2.2007.4 github.com/fxamacker/cbor/v2 v2.4.0 github.com/go-chi/chi v4.1.2+incompatible github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.9 github.com/google/go-tpm v0.3.3 github.com/google/uuid v1.3.0 - github.com/googleapis/gax-go/v2 v2.11.0 + github.com/googleapis/gax-go/v2 v2.12.0 github.com/hashicorp/vault/api v1.9.2 github.com/hashicorp/vault/api/auth/approle v0.4.1 github.com/hashicorp/vault/api/auth/kubernetes v0.4.1 github.com/micromdm/scep/v2 v2.1.0 - github.com/newrelic/go-agent/v3 v3.21.1 + github.com/newrelic/go-agent/v3 v3.23.1 github.com/pkg/errors v0.9.1 github.com/rs/xid v1.5.0 - github.com/sirupsen/logrus v1.9.2 + github.com/sirupsen/logrus v1.9.3 github.com/slackhq/nebula v1.6.1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 github.com/smallstep/nosql v0.6.0 github.com/stretchr/testify v1.8.4 - github.com/urfave/cli v1.22.13 + github.com/urfave/cli v1.22.14 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.6 go.step.sm/crypto v0.32.4 - go.step.sm/linkedca v0.19.1 + go.step.sm/linkedca v0.20.0 golang.org/x/crypto v0.11.0 golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 golang.org/x/net v0.12.0 - google.golang.org/api v0.130.0 - google.golang.org/grpc v1.56.1 + google.golang.org/api v0.132.0 + google.golang.org/grpc v1.56.2 google.golang.org/protobuf v1.31.0 gopkg.in/square/go-jose.v2 v2.6.0 ) require ( - cloud.google.com/go v0.110.2 // indirect - cloud.google.com/go/compute v1.19.3 // indirect + cloud.google.com/go v0.110.4 // indirect + cloud.google.com/go/compute v1.20.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.0 // indirect cloud.google.com/go/kms v1.13.0 // indirect @@ -64,8 +66,6 @@ require ( github.com/chzyer/readline v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgraph-io/badger v1.6.2 // indirect - github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dustin/go-humanize v1.0.0 // indirect @@ -106,8 +106,6 @@ require ( github.com/jackc/pgx/v4 v4.18.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.15.11 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect @@ -121,10 +119,8 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/ryboe/q v1.0.19 // indirect github.com/schollz/jsonstore v1.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect @@ -133,14 +129,14 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.etcd.io/bbolt v1.3.7 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/oauth2 v0.9.0 // indirect + golang.org/x/oauth2 v0.10.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.1.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect + google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -151,8 +147,7 @@ require ( // replace go.step.sm/linkedca => ../linkedca // use github.com/smallstep/pkcs7 fork with patches applied -//replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb - -replace go.mozilla.org/pkcs7 => ./../pkcs7 +replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb +// temporary replace until https://github.com/smallstep/linkedca/pull/55 is merged replace go.step.sm/linkedca => ./../linkedca diff --git a/go.sum b/go.sum index 34d84886..5fea32d0 100644 --- a/go.sum +++ b/go.sum @@ -31,16 +31,16 @@ cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aD cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= -cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk= +cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 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/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -50,16 +50,16 @@ cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94= cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= cloud.google.com/go/kms v1.13.0 h1:s+sRhcowXwuLsa2Z8g3Tmh5l0HWNBf//HogCgiuDs/0= cloud.google.com/go/kms v1.13.0/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= -cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE= -cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= +cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE= 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/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w= -cloud.google.com/go/security v1.14.1 h1:ZN+MFf1djt4VhuVd+JYoBjRftics3qKParPAXT5l4Uo= -cloud.google.com/go/security v1.14.1/go.mod h1:ItQAI0zVZd1OkHh+raoef892dsr7VY2QzMDJ4nOPtOs= +cloud.google.com/go/security v1.15.1 h1:jR3itwycg/TgGA0uIgTItcVhA55hKWiNJxaNNpQJaZE= +cloud.google.com/go/security v1.15.1/go.mod h1:MvTnnbsWnehoizHi09zoiZob0iCHVcL4AUBj76h9fXA= cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk= cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs= cloud.google.com/go/spanner v1.18.0/go.mod h1:LvAjUXPeJRGNuGpikMULjhLj/t9cRvdc+fxRoLiugXA= @@ -104,7 +104,7 @@ github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= @@ -493,8 +493,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= @@ -681,8 +681,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB 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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -780,8 +778,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/newrelic/go-agent/v3 v3.21.1 h1:nSLaQK+w/BHPUEpkPB+fX3ikgaRR2qyQiTECrcY+AmQ= -github.com/newrelic/go-agent/v3 v3.21.1/go.mod h1:AGagR69YHzamnvfxq9aDHnImvZwxr7C+4w7UN0Bm3UM= +github.com/newrelic/go-agent/v3 v3.23.1 h1:n4CK4EEod2A47T74wQFztavh9g3wHxxmlndj53ksbVg= +github.com/newrelic/go-agent/v3 v3.23.1/go.mod h1:dG7Q7yLUrqOo7SYVJADVDN9+P8c/87xp9axldPxmdHM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= @@ -826,7 +824,6 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -878,8 +875,6 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -895,8 +890,6 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb 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/ryboe/q v1.0.19 h1:1dO1anK4gorZRpXBD/edBZkMxIC1tFIwN03nfyOV13A= -github.com/ryboe/q v1.0.19/go.mod h1:IoEB3Q2/p6n1qbhIQVuNyakxtnV4rNJ/XJPK+jsEa0M= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -916,8 +909,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= -github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM= github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= @@ -926,6 +919,8 @@ github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 h1:h+cZ github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738/go.mod h1:mk2hyNbyai1oon+ilW9t42BuBVw7ee8elDdgrPq4394= github.com/smallstep/nosql v0.6.0 h1:ur7ysI8s9st0cMXnTvB8tA3+x5Eifmkb6hl4uqNV5jc= github.com/smallstep/nosql v0.6.0/go.mod h1:jOXwLtockXORUPPZ2MCUcIkGR6w0cN1QGZniY9DITQA= +github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb h1:wWc8z37baPz2oyusY9BVuM+uPtq6XAOb7qSegevnRs0= +github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= @@ -974,7 +969,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -998,8 +992,8 @@ github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oW 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.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.13 h1:wsLILXG8qCJNse/qAgLNf23737Cx05GflHg/PJGe1Ok= -github.com/urfave/cli v1.22.13/go.mod h1:VufqObjsMTF2BBwKawpx9R8eAneNEWhoO0yx8Vd+FkE= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= @@ -1237,8 +1231,8 @@ golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= -golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1492,8 +1486,8 @@ google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtuk google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.130.0 h1:A50ujooa1h9iizvfzA4rrJr2B7uRmWexwbekQ2+5FPQ= -google.golang.org/api v0.130.0/go.mod h1:J/LCJMYSDFvAVREGCbrESb53n4++NMBDetSHGL5I5RY= +google.golang.org/api v0.132.0 h1:8t2/+qZ26kAOGSmOiHwVycqVaDg7q3JDILrNi/Z6rvc= +google.golang.org/api v0.132.0/go.mod h1:AeTBC6GpJnJSRJjktDcPX0QwtS8pGYZOV6MSuSCusw0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1571,12 +1565,12 @@ google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKr google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= 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= @@ -1612,8 +1606,8 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= -google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= +google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= 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= diff --git a/scripts/README.md b/scripts/README.md index 80d3cdba..5571bf86 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -2,3 +2,7 @@ Please note that `install-step-ra.sh` is referenced on the `files.smallstep.com` S3 website bucket as a redirect to `raw.githubusercontent.com`. If you move it, please update the S3 redirect. +## badger-migration + +badger-migration is a tool that allows migrating data data from BadgerDB (v1 or +v2) to MySQL or PostgreSQL. diff --git a/scripts/badger-migration/main.go b/scripts/badger-migration/main.go new file mode 100644 index 00000000..89fb8e7d --- /dev/null +++ b/scripts/badger-migration/main.go @@ -0,0 +1,352 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + + badgerv1 "github.com/dgraph-io/badger" + badgerv2 "github.com/dgraph-io/badger/v2" + + "github.com/smallstep/nosql" +) + +var ( + authorityTables = []string{ + "x509_certs", + "x509_certs_data", + "revoked_x509_certs", + "x509_crl", + "revoked_ssh_certs", + "used_ott", + "ssh_certs", + "ssh_hosts", + "ssh_users", + "ssh_host_principals", + } + acmeTables = []string{ + "acme_accounts", + "acme_keyID_accountID_index", + "acme_authzs", + "acme_challenges", + "nonces", + "acme_orders", + "acme_account_orders_index", + "acme_certs", + "acme_serial_certs_index", + "acme_external_account_keys", + "acme_external_account_keyID_reference_index", + "acme_external_account_keyID_provisionerID_index", + } + adminTables = []string{ + "admins", + "provisioners", + "authority_policies", + } +) + +type DB interface { + CreateTable([]byte) error + Set(bucket, key, value []byte) error +} + +type dryRunDB struct{} + +func (*dryRunDB) CreateTable([]byte) error { return nil } +func (*dryRunDB) Set(bucket, key, value []byte) error { return nil } + +func usage(fs *flag.FlagSet) { + name := filepath.Base(os.Args[0]) + fmt.Fprintf(os.Stderr, "%s is a tool to migrate data from BadgerDB to MySQL or PostgreSQL.\n", name) + fmt.Fprintln(os.Stderr, "\nUsage:") + fmt.Fprintf(os.Stderr, " %s [-v1|-v2] -dir= [-value-dir=] -type=type -database=\n", name) + fmt.Fprintln(os.Stderr, "\nExamples:") + fmt.Fprintf(os.Stderr, " %s -v1 -dir /var/lib/step-ca/db -type=mysql -database \"user@unix/step_ca\"\n", name) + fmt.Fprintf(os.Stderr, " %s -v1 -dir /var/lib/step-ca/db -type=mysql -database \"user:password@tcp(localhost:3306)/step_ca\"\n", name) + fmt.Fprintf(os.Stderr, " %s -v2 -dir /var/lib/step-ca/db -type=postgresql -database \"user=postgres dbname=step_ca\"\n", name) + fmt.Fprintf(os.Stderr, " %s -v2 -dir /var/lib/step-ca/db -dry-run\"\n", name) + fmt.Fprintln(os.Stderr, "\nOptions:") + fs.PrintDefaults() +} + +func main() { + var v1, v2, dryRun bool + var dir, valueDir string + var typ, database string + var key string + + fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + + fs.BoolVar(&v1, "v1", false, "use badger v1 as the source database") + fs.BoolVar(&v2, "v2", false, "use badger v2 as the source database") + fs.StringVar(&dir, "dir", "", "badger database directory") + fs.StringVar(&valueDir, "value-dir", "", "badger database value directory") + fs.StringVar(&typ, "type", "", "the destination database type to use") + fs.StringVar(&database, "database", "", "the destination driver-specific data source name") + fs.StringVar(&key, "key", "", "the key used to resume the migration") + fs.BoolVar(&dryRun, "dry-run", false, "runs the migration scripts without writing anything") + fs.Usage = func() { usage(fs) } + fs.Parse(os.Args[1:]) + + switch { + case v1 == v2: + fatal("flag -v1 or -v2 are required") + case dir == "": + fatal("flag -dir is required") + case typ != "postgresql" && typ != "mysql" && !dryRun: + fatal(`flag -type must be "postgresql" or "mysql"`) + case database == "" && !dryRun: + fatal("flag --database required") + } + + var ( + err error + v1DB *badgerv1.DB + v2DB *badgerv2.DB + lastKey []byte + ) + + if key != "" { + if lastKey, err = base64.StdEncoding.DecodeString(key); err != nil { + fatal("error decoding key: %v", err) + } + } + + if v1 { + if v1DB, err = badgerV1Open(dir, valueDir); err != nil { + fatal("error opening badger v1 database: %v", err) + } + } else { + if v2DB, err = badgerV2Open(dir, valueDir); err != nil { + fatal("error opening badger v2 database: %v", err) + } + } + + var db DB + if dryRun { + db = &dryRunDB{} + } else { + db, err = nosql.New(typ, database) + if err != nil { + fatal("error opening %s database: %v", typ, err) + } + } + + allTables := append([]string{}, authorityTables...) + allTables = append(allTables, acmeTables...) + allTables = append(allTables, adminTables...) + + // Convert prefix names to badger key prefixes + badgerKeys := make([][]byte, len(allTables)) + for i, name := range allTables { + badgerKeys[i], err = badgerEncode([]byte(name)) + if err != nil { + fatal("error encoding table %s: %v", name, err) + } + } + + for i, prefix := range badgerKeys { + table := allTables[i] + + // With a key flag, resume from that table and prefix + if lastKey != nil { + bucket, _ := parseBadgerEncode(lastKey) + if table != string(bucket) { + fmt.Printf("skipping table %s\n", table) + continue + } + // Continue with a new prefix + prefix = lastKey + lastKey = nil + } + + var n int64 + fmt.Printf("migrating %s ...", table) + if err := db.CreateTable([]byte(table)); err != nil { + fatal("error creating table %s: %v", table, err) + } + + if v1 { + if badgerKey, err := badgerV1Iterate(v1DB, prefix, func(bucket, key, value []byte) error { + n++ + return db.Set(bucket, key, value) + }); err != nil { + fmt.Println() + fatal("error inserting into %s: %v\nLast key: %s", table, err, base64.StdEncoding.EncodeToString(badgerKey)) + } + } else { + if badgerKey, err := badgerV2Iterate(v2DB, prefix, func(bucket, key, value []byte) error { + n++ + return db.Set(bucket, key, value) + }); err != nil { + fmt.Println() + fatal("error inserting into %s: %v\nLast key: %s", table, err, base64.StdEncoding.EncodeToString(badgerKey)) + } + } + + fmt.Printf(" %d rows\n", n) + } +} + +func fatal(format string, args ...any) { + fmt.Fprintf(os.Stderr, format, args...) + fmt.Fprintln(os.Stderr) + os.Exit(1) +} + +func badgerV1Open(dir, valueDir string) (*badgerv1.DB, error) { + opts := badgerv1.DefaultOptions(dir) + if valueDir != "" { + opts.ValueDir = valueDir + } + return badgerv1.Open(opts) +} + +func badgerV2Open(dir, valueDir string) (*badgerv2.DB, error) { + opts := badgerv2.DefaultOptions(dir) + if valueDir != "" { + opts.ValueDir = valueDir + } + return badgerv2.Open(opts) +} + +type Iterator interface { + Seek([]byte) + ValidForPrefix([]byte) bool + Next() +} + +type Item interface { + KeyCopy([]byte) []byte + ValueCopy([]byte) ([]byte, error) +} + +func badgerV1Iterate(db *badgerv1.DB, prefix []byte, fn func(bucket, key, value []byte) error) (badgerKey []byte, err error) { + err = db.View(func(txn *badgerv1.Txn) error { + it := txn.NewIterator(badgerv1.DefaultIteratorOptions) + defer it.Close() + badgerKey, err = badgerIterate(it, prefix, fn) + return err + }) + return +} + +func badgerV2Iterate(db *badgerv2.DB, prefix []byte, fn func(bucket, key, value []byte) error) (badgerKey []byte, err error) { + err = db.View(func(txn *badgerv2.Txn) error { + it := txn.NewIterator(badgerv2.DefaultIteratorOptions) + defer it.Close() + badgerKey, err = badgerIterate(it, prefix, fn) + return err + }) + return +} + +func badgerIterate(it Iterator, prefix []byte, fn func(bucket, key, value []byte) error) ([]byte, error) { + var badgerKey []byte + for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { + var item Item + switch itt := it.(type) { + case *badgerv1.Iterator: + item = itt.Item() + case *badgerv2.Iterator: + item = itt.Item() + default: + return badgerKey, fmt.Errorf("unexpected iterator type %T", it) + } + + badgerKey = item.KeyCopy(nil) + if isBadgerTable(badgerKey) { + continue + } + + bucket, key, err := fromBadgerKey(badgerKey) + if err != nil { + return badgerKey, fmt.Errorf("error converting from badger key %s", badgerKey) + } + value, err := item.ValueCopy(nil) + if err != nil { + return badgerKey, fmt.Errorf("error retrieving contents from database value: %w", err) + } + + if err := fn(bucket, key, value); err != nil { + return badgerKey, fmt.Errorf("error exporting %s[%s]=%x", bucket, key, value) + } + } + + return badgerKey, nil +} + +// badgerEncode encodes a byte slice into a section of a BadgerKey. See +// documentation for toBadgerKey. +func badgerEncode(val []byte) ([]byte, error) { + l := len(val) + switch { + case l == 0: + return nil, errors.New("input cannot be empty") + case l > 65535: + return nil, errors.New("length of input cannot be greater than 65535") + default: + lb := new(bytes.Buffer) + if err := binary.Write(lb, binary.LittleEndian, uint16(l)); err != nil { + return nil, fmt.Errorf("error doing binary Write: %w", err) + } + return append(lb.Bytes(), val...), nil + } +} + +// parseBadgerEncode decodes the badger key and returns the bucket and the rest. +func parseBadgerEncode(bk []byte) (value, rest []byte) { + var ( + keyLen uint16 + start = uint16(2) + length = uint16(len(bk)) + ) + if uint16(len(bk)) < start { + return nil, bk + } + // First 2 bytes stores the length of the value. + if err := binary.Read(bytes.NewReader(bk[:2]), binary.LittleEndian, &keyLen); err != nil { + return nil, bk + } + end := start + keyLen + switch { + case length < end: + return nil, bk + case length == end: + return bk[start:end], nil + default: + return bk[start:end], bk[end:] + } +} + +// isBadgerTable returns True if the slice is a badgerTable token, false +// otherwise. badgerTable means that the slice contains only the [size|value] of +// one section of a badgerKey and no remainder. A badgerKey is [bucket|key], +// while a badgerTable is only the bucket section. +func isBadgerTable(bk []byte) bool { + if k, rest := parseBadgerEncode(bk); len(k) > 0 && len(rest) == 0 { + return true + } + return false +} + +// fromBadgerKey returns the bucket and key encoded in a BadgerKey. See +// documentation for toBadgerKey. +func fromBadgerKey(bk []byte) ([]byte, []byte, error) { + bucket, rest := parseBadgerEncode(bk) + if len(bucket) == 0 || len(rest) == 0 { + return nil, nil, fmt.Errorf("invalid badger key: %v", bk) + } + + key, rest2 := parseBadgerEncode(rest) + if len(key) == 0 || len(rest2) != 0 { + return nil, nil, fmt.Errorf("invalid badger key: %v", bk) + } + + return bucket, key, nil +} diff --git a/webhook/options.go b/webhook/options.go index 88c44986..86923709 100644 --- a/webhook/options.go +++ b/webhook/options.go @@ -68,6 +68,13 @@ func WithAttestationData(data *AttestationData) RequestBodyOption { } } +func WithAuthorizationPrincipal(p string) RequestBodyOption { + return func(rb *RequestBody) error { + rb.AuthorizationPrincipal = p + return nil + } +} + func WithSSHCertificateRequest(cr sshutil.CertificateRequest) RequestBodyOption { return func(rb *RequestBody) error { rb.SSHCertificateRequest = &SSHCertificateRequest{ @@ -95,3 +102,23 @@ func WithSSHCertificate(cert *sshutil.Certificate, certTpl *ssh.Certificate) Req return nil } } + +func WithX5CCertificate(leaf *x509.Certificate) RequestBodyOption { + return func(rb *RequestBody) error { + rb.X5CCertificate = &X5CCertificate{ + Raw: leaf.Raw, + PublicKeyAlgorithm: leaf.PublicKeyAlgorithm.String(), + NotBefore: leaf.NotBefore, + NotAfter: leaf.NotAfter, + } + if leaf.PublicKey != nil { + key, err := x509.MarshalPKIXPublicKey(leaf.PublicKey) + if err != nil { + return err + } + rb.X5CCertificate.PublicKey = key + } + + return nil + } +} diff --git a/webhook/options_test.go b/webhook/options_test.go index e813bb44..9bcc59bc 100644 --- a/webhook/options_test.go +++ b/webhook/options_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/smallstep/assert" + "go.step.sm/crypto/keyutil" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" @@ -16,6 +17,15 @@ func TestNewRequestBody(t *testing.T) { t1 := time.Now() t2 := t1.Add(time.Hour) + key, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + keyBytes, err := x509.MarshalPKIXPublicKey(key.Public()) + if err != nil { + t.Fatal(err) + } + type test struct { options []RequestBodyOption want *RequestBody @@ -103,6 +113,40 @@ func TestNewRequestBody(t *testing.T) { }, wantErr: false, }, + "X5C Certificate": { + options: []RequestBodyOption{ + WithX5CCertificate(&x509.Certificate{ + Raw: []byte("some raw data"), + NotBefore: t1, + NotAfter: t2, + PublicKeyAlgorithm: x509.ECDSA, + PublicKey: key.Public(), + }), + }, + want: &RequestBody{ + X5CCertificate: &X5CCertificate{ + Raw: []byte("some raw data"), + PublicKeyAlgorithm: "ECDSA", + NotBefore: t1, + NotAfter: t2, + PublicKey: keyBytes, + }, + }, + wantErr: false, + }, + "fail/X5C Certificate": { + options: []RequestBodyOption{ + WithX5CCertificate(&x509.Certificate{ + Raw: []byte("some raw data"), + NotBefore: t1, + NotAfter: t2, + PublicKeyAlgorithm: x509.ECDSA, + PublicKey: []byte("fail"), + }), + }, + want: nil, + wantErr: true, + }, } for name, test := range tests { t.Run(name, func(t *testing.T) { diff --git a/webhook/types.go b/webhook/types.go index 9605742a..9eda0578 100644 --- a/webhook/types.go +++ b/webhook/types.go @@ -56,6 +56,17 @@ type AttestationData struct { PermanentIdentifier string `json:"permanentIdentifier"` } +// X5CCertificate is the authorization certificate sent to webhook servers for +// enriching or authorizing webhooks when signing X509 or SSH certificates using +// the X5C provisioner. +type X5CCertificate struct { + Raw []byte `json:"raw"` + PublicKey []byte `json:"publicKey"` + PublicKeyAlgorithm string `json:"publicKeyAlgorithm"` + NotBefore time.Time `json:"notBefore"` + NotAfter time.Time `json:"notAfter"` +} + // RequestBody is the body sent to webhook servers. type RequestBody struct { Timestamp time.Time `json:"timestamp"` @@ -71,4 +82,8 @@ type RequestBody struct { // Only set for SCEP challenge validation requests SCEPChallenge string `json:"scepChallenge,omitempty"` SCEPTransactionID string `json:"scepTransactionID,omitempty"` + // Only set for X5C provisioners + X5CCertificate *X5CCertificate `json:"x5cCertificate,omitempty"` + // Set for X5C, AWS, GCP, and Azure provisioners + AuthorizationPrincipal string `json:"authorizationPrincipal,omitempty"` }