diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 538aed15..00000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,4 +0,0 @@ -needs triage: - - '**' # index.php | src/main.php - - '.*' # .gitignore - - '.*/**' # .github/workflows/label.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a67b766f..6273c13e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: pull_request: workflow_call: secrets: - GITLEAKS_LICENSE_KEY: + CODECOV_TOKEN: required: true concurrency: @@ -23,5 +23,4 @@ jobs: os-dependencies: "libpcsclite-dev" run-gitleaks: true run-codeql: true - secrets: - GITLEAKS_LICENSE_KEY: ${{ secrets.GITLEAKS_LICENSE_KEY }} + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd434737..4c00ad04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,25 +8,17 @@ on: jobs: ci: - uses: smallstep/certificates/.github/workflows/ci.yml@main + uses: smallstep/certificates/.github/workflows/ci.yml@master secrets: inherit create_release: name: Create Release - needs: ci + #needs: ci runs-on: ubuntu-20.04 outputs: - debversion: ${{ steps.extract-tag.outputs.DEB_VERSION }} is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} steps: - - - name: Extract Tag Names - id: extract-tag - run: | - DEB_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | sed 's/-/./') - echo "::set-output name=DEB_VERSION::${DEB_VERSION}" - - - name: Is Pre-release + - name: Is Pre-release id: is_prerelease run: | set +e @@ -34,8 +26,7 @@ jobs: OUT=$? if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi echo "::set-output name=IS_PRERELEASE::${IS_PRERELEASE}" - - - name: Create Release + - name: Create Release id: create_release uses: actions/create-release@v1 env: @@ -51,54 +42,33 @@ jobs: runs-on: ubuntu-20.04 needs: create_release steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 with: go-version: 1.19 - - - name: APT Install - id: aptInstall - run: sudo apt-get -y install build-essential debhelper fakeroot - - - name: Build Debian package - id: make_debian - run: | - PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin - make debian - # need to restore the git state otherwise goreleaser fails due to dirty state - git restore debian/changelog - git clean -fd - - - name: Install cosign - uses: sigstore/cosign-installer@v1.1.0 + check-latest: true + - name: Install cosign + uses: sigstore/cosign-installer@v2.7.0 with: - cosign-release: 'v1.1.0' - - - name: Write cosign key to disk + cosign-release: 'v1.12.1' + - name: Write cosign key to disk id: write_key run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key" - - - name: Get Release Date + - name: Get Release Date id: release_date run: | RELEASE_DATE=$(date +"%y-%m-%d") echo "::set-output name=RELEASE_DATE::${RELEASE_DATE}" - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@5a54d7e660bda43b405e8463261b3d25631ffe86 # v2.7.0 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v3 with: - version: 'v1.7.0' + version: 'latest' args: release --rm-dist env: - GITHUB_TOKEN: ${{ secrets.PAT }} + GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }} COSIGN_PWD: ${{ secrets.COSIGN_PWD }} - DEB_VERSION: ${{ needs.create_release.outputs.debversion }} RELEASE_DATE: ${{ steps.release_date.outputs.RELEASE_DATE }} build_upload_docker: @@ -106,25 +76,21 @@ jobs: runs-on: ubuntu-20.04 needs: ci steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Go - uses: actions/setup-go@v2 + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Go + uses: actions/setup-go@v3 with: go-version: '1.19' - - - name: Install cosign + check-latest: true + - name: Install cosign uses: sigstore/cosign-installer@v1.1.0 with: cosign-release: 'v1.1.0' - - - name: Write cosign key to disk + - name: Write cosign key to disk id: write_key run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key" - - - name: Build + - name: Build id: build run: | PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index c16aacd8..f1363a4b 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -4,26 +4,13 @@ on: issues: types: - opened + - reopened pull_request_target: types: - opened + - reopened jobs: - - label: - name: Label PR - runs-on: ubuntu-latest - if: github.event_name == 'pull_request_target' - steps: - - uses: actions/labeler@v3.0.2 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - - add-to-project: - name: Add to Triage Project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.3.0 - with: - project-url: https://github.com/orgs/smallstep/projects/94 - github-token: ${{ secrets.TRIAGE_PAT }} + triage: + uses: smallstep/workflows/.github/workflows/triage.yml@main + secrets: inherit diff --git a/.goreleaser.yml b/.goreleaser.yml index 7d57e657..c8650d5b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -26,49 +26,7 @@ builds: flags: - -trimpath main: ./cmd/step-ca/main.go - binary: bin/step-ca - ldflags: - - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - - - id: step-cloudkms-init - env: - - CGO_ENABLED=0 - targets: - - darwin_amd64 - - darwin_arm64 - - freebsd_amd64 - - linux_386 - - linux_amd64 - - linux_arm64 - - linux_arm_5 - - linux_arm_6 - - linux_arm_7 - - windows_amd64 - flags: - - -trimpath - main: ./cmd/step-cloudkms-init/main.go - binary: bin/step-cloudkms-init - ldflags: - - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - - - id: step-awskms-init - env: - - CGO_ENABLED=0 - targets: - - darwin_amd64 - - darwin_arm64 - - freebsd_amd64 - - linux_386 - - linux_amd64 - - linux_arm64 - - linux_arm_5 - - linux_arm_6 - - linux_arm_7 - - windows_amd64 - flags: - - -trimpath - main: ./cmd/step-awskms-init/main.go - binary: bin/step-awskms-init + binary: step-ca ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} @@ -85,6 +43,38 @@ archives: files: - README.md - LICENSE + allow_different_binary_count: true + +nfpms: + # Configure nFPM for .deb and .rpm releases + # + # See https://nfpm.goreleaser.com/configuration/ + # and https://goreleaser.com/customization/nfpm/ + # + # Useful tools for debugging .debs: + # List file contents: dpkg -c dist/step_...deb + # Package metadata: dpkg --info dist/step_....deb + # + - + builds: + - step-ca + package_name: step-ca + file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" + vendor: Smallstep Labs + homepage: https://github.com/smallstep/certificates + maintainer: Smallstep + description: > + step-ca is an online certificate authority for secure, automated certificate management. + license: Apache 2.0 + section: utils + formats: + - deb + - rpm + priority: optional + bindir: /usr/bin + contents: + - src: debian/copyright + dst: /usr/share/doc/step-ca/copyright source: enabled: true @@ -98,7 +88,7 @@ checksum: signs: - cmd: cosign stdin: '{{ .Env.COSIGN_PWD }}' - args: ["sign-blob", "-key=/tmp/cosign.key", "-output=${signature}", "${artifact}"] + args: ["sign-blob", "-key=/tmp/cosign.key", "-output-signature=${signature}", "${artifact}"] artifacts: all snapshot: @@ -140,7 +130,7 @@ release: #### Linux - 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz) - - 📦 [step-ca_{{ .Env.DEB_VERSION }}_amd64.deb](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Env.DEB_VERSION }}_amd64.deb) + - 📦 [step-ca_{{ .Version }}_amd64.deb](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Version }}_amd64.deb) #### OSX Darwin @@ -194,39 +184,3 @@ release: # - glob: ./path/to/file.txt # - 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 }}" - - # 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 - - # 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" - - # 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 license - # Default is empty. - license: "Apache-2.0" diff --git a/.version.sh b/.version.sh index 14adccbf..e7f823cd 100755 --- a/.version.sh +++ b/.version.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh read -r firstline < .VERSION last_half="${firstline##*tag: }" if [[ ${last_half::1} == "v" ]]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f50379..59829021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added support for ACME device-attest-01 challenge. - Added name constraints evaluation and enforcement when issuing or renewing X.509 certificates. +- Added provisioner webhooks for augmenting template data and authorizing certificate requests before signing. +- Added automatic migration of provisioners when enabling remote managment. + +### Fixed +- MySQL DSN parsing issues fixed with upgrade to [smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0). ## [0.22.1] - 2022-08-31 ### Fixed diff --git a/acme/challenge_test.go b/acme/challenge_test.go index e452b175..0027f5b1 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -2774,3 +2774,97 @@ func Test_doStepAttestationFormat(t *testing.T) { }) } } + +func Test_doStepAttestationFormat_noCAIntermediate(t *testing.T) { + ctx := context.Background() + + // This CA simulates a YubiKey v5.2.4, where the attestation intermediate in + // the CA does not have the basic constraint extension. With the current + // validation of the certificate the test case below returns an error. If + // we change the validation to support this use case, the test case below + // should change. + // + // See https://github.com/Yubico/yubikey-manager/issues/522 + ca, err := minica.New(minica.WithIntermediateTemplate(`{"subject": {{ toJson .Subject }}}`)) + if err != nil { + t.Fatal(err) + } + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) + + makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate { + leaf, err := ca.Sign(&x509.Certificate{ + Subject: pkix.Name{CommonName: "attestation cert"}, + PublicKey: signer.Public(), + ExtraExtensions: []pkix.Extension{ + {Id: oidYubicoSerialNumber, Value: serialNumber}, + }, + }) + if err != nil { + t.Fatal(err) + } + return leaf + } + + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + serialNumber, err := asn1.Marshal(1234) + if err != nil { + t.Fatal(err) + } + leaf := makeLeaf(signer, serialNumber) + + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + if err != nil { + t.Fatal(err) + } + keyAuth, err := KeyAuthorization("token", jwk) + if err != nil { + t.Fatal(err) + } + keyAuthSum := sha256.Sum256([]byte(keyAuth)) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + if err != nil { + t.Fatal(err) + } + cborSig, err := cbor.Marshal(sig) + if err != nil { + t.Fatal(err) + } + + type args struct { + ctx context.Context + prov Provisioner + ch *Challenge + jwk *jose.JSONWebKey + att *AttestationObject + } + tests := []struct { + name string + args args + want *stepAttestationData + wantErr bool + }{ + {"fail no intermediate", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + Format: "step", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, + "alg": -7, + "sig": cborSig, + }, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att) + if (err != nil) != tt.wantErr { + t.Errorf("doStepAttestationFormat() error = %#v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("doStepAttestationFormat() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/acme/errors.go b/acme/errors.go index c7c6c2cf..a969bd96 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -75,6 +75,8 @@ func (ap ProblemType) String() string { return "accountDoesNotExist" case ErrorAlreadyRevokedType: return "alreadyRevoked" + case ErrorBadAttestationStatementType: + return "badAttestationStatement" case ErrorBadCSRType: return "badCSR" case ErrorBadNonceType: diff --git a/acme/order.go b/acme/order.go index 96c925f1..7748df22 100644 --- a/acme/order.go +++ b/acme/order.go @@ -194,6 +194,14 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques if err != nil { return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner") } + // Unlike most of the provisioners, ACME's AuthorizeSign method doesn't + // define the templates, and the template data used in WebHooks is not + // available. + for _, signOp := range signOps { + if wc, ok := signOp.(*provisioner.WebhookController); ok { + wc.TemplateData = data + } + } templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate) if err != nil { diff --git a/api/renew.go b/api/renew.go index 3cfd5fdf..6e9f680f 100644 --- a/api/renew.go +++ b/api/renew.go @@ -17,7 +17,6 @@ const ( // Renew uses the information of certificate in the TLS connection to create a // new one. func Renew(w http.ResponseWriter, r *http.Request) { - //nolint:contextcheck // the reqest has the context cert, err := getPeerCertificate(r) if err != nil { render.Error(w, err) diff --git a/api/sshRekey.go b/api/sshRekey.go index 977c4719..6c0a5064 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -83,7 +83,6 @@ func SSHRekey(w http.ResponseWriter, r *http.Request) { notBefore := time.Unix(int64(oldCert.ValidAfter), 0) notAfter := time.Unix(int64(oldCert.ValidBefore), 0) - //nolint:contextcheck // the reqest has the context identity, err := renewIdentityCertificate(r, notBefore, notAfter) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate")) diff --git a/api/sshRenew.go b/api/sshRenew.go index 456be3f6..4e4d0b04 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -75,7 +75,6 @@ func SSHRenew(w http.ResponseWriter, r *http.Request) { notBefore := time.Unix(int64(oldCert.ValidAfter), 0) notAfter := time.Unix(int64(oldCert.ValidBefore), 0) - //nolint:contextcheck // the reqest has the context identity, err := renewIdentityCertificate(r, notBefore, notAfter) if err != nil { render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate")) diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 1e5919ce..a4faf936 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -4,41 +4,47 @@ import ( "context" "net/http" - "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" - "github.com/smallstep/certificates/authority/admin" ) -// Handler is the Admin API request handler. -type Handler struct { - acmeResponder ACMEAdminResponder - policyResponder PolicyAdminResponder -} - -// Route traffic and implement the Router interface. -// -// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) -func (h *Handler) Route(r api.Router) { - Route(r, h.acmeResponder, h.policyResponder) -} - -// NewHandler returns a new Authority Config Handler. -// -// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) -func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) api.RouterHandler { - return &Handler{ - acmeResponder: acmeResponder, - policyResponder: policyResponder, - } -} - var mustAuthority = func(ctx context.Context) adminAuthority { return authority.MustFromContext(ctx) } +type router struct { + acmeResponder ACMEAdminResponder + policyResponder PolicyAdminResponder + webhookResponder WebhookAdminResponder +} + +type RouterOption func(*router) + +func WithACMEResponder(acmeResponder ACMEAdminResponder) RouterOption { + return func(r *router) { + r.acmeResponder = acmeResponder + } +} + +func WithPolicyResponder(policyResponder PolicyAdminResponder) RouterOption { + return func(r *router) { + r.policyResponder = policyResponder + } +} + +func WithWebhookResponder(webhookResponder WebhookAdminResponder) RouterOption { + return func(r *router) { + r.webhookResponder = webhookResponder + } +} + // Route traffic and implement the Router interface. -func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) { +func Route(r api.Router, options ...RouterOption) { + router := &router{} + for _, fn := range options { + fn(router) + } + authnz := func(next http.HandlerFunc) http.HandlerFunc { return extractAuthorizeTokenAdmin(requireAPIEnabled(next)) } @@ -67,6 +73,10 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next))))) } + webhookMiddleware := func(next http.HandlerFunc) http.HandlerFunc { + return authnz(loadProvisionerByName(next)) + } + // Provisioners r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner)) r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners)) @@ -82,36 +92,42 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin)) // ACME responder - if acmeResponder != nil { + if router.acmeResponder != nil { // ACME External Account Binding Keys - r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys)) - r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys)) - r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey)) - r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey)) + r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys)) + r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys)) + r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.CreateExternalAccountKey)) + r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(router.acmeResponder.DeleteExternalAccountKey)) } // Policy responder - if policyResponder != nil { + if router.policyResponder != nil { // Policy - Authority - r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy)) - r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy)) - r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy)) - r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy)) + r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(router.policyResponder.GetAuthorityPolicy)) + r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(router.policyResponder.CreateAuthorityPolicy)) + r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(router.policyResponder.UpdateAuthorityPolicy)) + r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(router.policyResponder.DeleteAuthorityPolicy)) // Policy - Provisioner - r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy)) - r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy)) - r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy)) - r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy)) + r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.GetProvisionerPolicy)) + r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.CreateProvisionerPolicy)) + r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.UpdateProvisionerPolicy)) + r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.DeleteProvisionerPolicy)) // Policy - ACME Account - r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy)) - r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy)) - r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy)) - r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy)) - r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy)) - r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy)) - r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy)) - r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy)) + r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy)) + r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy)) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy)) + r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy)) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy)) + r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy)) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy)) + r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy)) + } + + if router.webhookResponder != nil { + r.MethodFunc("POST", "/provisioners/{provisionerName}/webhooks", webhookMiddleware(router.webhookResponder.CreateProvisionerWebhook)) + r.MethodFunc("PUT", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.UpdateProvisionerWebhook)) + r.MethodFunc("DELETE", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.DeleteProvisionerWebhook)) } } diff --git a/authority/admin/api/webhook.go b/authority/admin/api/webhook.go new file mode 100644 index 00000000..f73f6806 --- /dev/null +++ b/authority/admin/api/webhook.go @@ -0,0 +1,235 @@ +package api + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + + "github.com/go-chi/chi" + "github.com/smallstep/certificates/api/read" + "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/admin" + "go.step.sm/crypto/randutil" + "go.step.sm/linkedca" +) + +// WebhookAdminResponder is the interface responsible for writing webhook admin +// responses. +type WebhookAdminResponder interface { + CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request) + UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request) + DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request) +} + +// webhoookAdminResponder implements WebhookAdminResponder +type webhookAdminResponder struct{} + +// NewWebhookAdminResponder returns a new WebhookAdminResponder +func NewWebhookAdminResponder() WebhookAdminResponder { + return &webhookAdminResponder{} +} + +func validateWebhook(webhook *linkedca.Webhook) error { + if webhook == nil { + return nil + } + + // name + if webhook.Name == "" { + return admin.NewError(admin.ErrorBadRequestType, "webhook name is required") + } + + // url + parsedURL, err := url.Parse(webhook.Url) + if err != nil { + return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid") + } + if parsedURL.Host == "" { + return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid") + } + if parsedURL.Scheme != "https" { + return admin.NewError(admin.ErrorBadRequestType, "webhook url must use https") + } + if parsedURL.User != nil { + return admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password") + } + + // kind + switch webhook.Kind { + case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING: + default: + return admin.NewError(admin.ErrorBadRequestType, "webhook kind is invalid") + } + + return nil +} + +func (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + auth := mustAuthority(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) + + var newWebhook = new(linkedca.Webhook) + if err := read.ProtoJSON(r.Body, newWebhook); err != nil { + render.Error(w, err) + return + } + + if err := validateWebhook(newWebhook); err != nil { + render.Error(w, err) + return + } + if newWebhook.Secret != "" { + err := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set") + render.Error(w, err) + return + } + if newWebhook.Id != "" { + err := admin.NewError(admin.ErrorBadRequestType, "webhook ID must not be set") + render.Error(w, err) + return + } + + id, err := randutil.UUIDv4() + if err != nil { + render.Error(w, admin.WrapErrorISE(err, "error generating webhook id")) + return + } + newWebhook.Id = id + + // verify the name is unique + for _, wh := range prov.Webhooks { + if wh.Name == newWebhook.Name { + err := admin.NewError(admin.ErrorConflictType, "provisioner %q already has a webhook with the name %q", prov.Name, newWebhook.Name) + render.Error(w, err) + return + } + } + + secret, err := randutil.Bytes(64) + if err != nil { + render.Error(w, admin.WrapErrorISE(err, "error generating webhook secret")) + return + } + newWebhook.Secret = base64.StdEncoding.EncodeToString(secret) + + prov.Webhooks = append(prov.Webhooks, newWebhook) + + if err := auth.UpdateProvisioner(ctx, prov); err != nil { + if isBadRequest(err) { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner webhook")) + return + } + + render.Error(w, admin.WrapErrorISE(err, "error creating provisioner webhook")) + return + } + + render.ProtoJSONStatus(w, newWebhook, http.StatusCreated) +} + +func (war *webhookAdminResponder) DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + auth := mustAuthority(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) + + webhookName := chi.URLParam(r, "webhookName") + + found := false + for i, wh := range prov.Webhooks { + if wh.Name == webhookName { + prov.Webhooks = append(prov.Webhooks[0:i], prov.Webhooks[i+1:]...) + found = true + break + } + } + if !found { + render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) + return + } + + if err := auth.UpdateProvisioner(ctx, prov); err != nil { + if isBadRequest(err) { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error deleting provisioner webhook")) + return + } + + render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner webhook")) + return + } + + render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) +} + +func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + auth := mustAuthority(ctx) + prov := linkedca.MustProvisionerFromContext(ctx) + + var newWebhook = new(linkedca.Webhook) + if err := read.ProtoJSON(r.Body, newWebhook); err != nil { + render.Error(w, err) + return + } + + if err := validateWebhook(newWebhook); err != nil { + render.Error(w, err) + return + } + + found := false + for i, wh := range prov.Webhooks { + if wh.Name != newWebhook.Name { + continue + } + if newWebhook.Secret != "" && newWebhook.Secret != wh.Secret { + err := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated") + render.Error(w, err) + return + } + newWebhook.Secret = wh.Secret + if newWebhook.Id != "" && newWebhook.Id != wh.Id { + err := admin.NewError(admin.ErrorBadRequestType, "webhook ID cannot be updated") + render.Error(w, err) + return + } + newWebhook.Id = wh.Id + prov.Webhooks[i] = newWebhook + found = true + break + } + if !found { + msg := fmt.Sprintf("provisioner %q has no webhook with the name %q", prov.Name, newWebhook.Name) + err := admin.NewError(admin.ErrorNotFoundType, msg) + render.Error(w, err) + return + } + + if err := auth.UpdateProvisioner(ctx, prov); err != nil { + if isBadRequest(err) { + render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner webhook")) + return + } + + render.Error(w, admin.WrapErrorISE(err, "error updating provisioner webhook")) + return + } + + // Return a copy without the signing secret. Include the client-supplied + // auth secrets since those may have been updated in this request and we + // should show in the response that they changed + whResponse := &linkedca.Webhook{ + Id: newWebhook.Id, + Name: newWebhook.Name, + Url: newWebhook.Url, + Kind: newWebhook.Kind, + CertType: newWebhook.CertType, + Auth: newWebhook.Auth, + DisableTlsClientAuth: newWebhook.DisableTlsClientAuth, + } + render.ProtoJSONStatus(w, whResponse, http.StatusCreated) +} diff --git a/authority/admin/api/webhook_test.go b/authority/admin/api/webhook_test.go new file mode 100644 index 00000000..baac2c11 --- /dev/null +++ b/authority/admin/api/webhook_test.go @@ -0,0 +1,668 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/admin" + "github.com/stretchr/testify/assert" + "go.step.sm/linkedca" + "google.golang.org/protobuf/encoding/protojson" +) + +// ignore secret and id since those are set by the server +func assertEqualWebhook(t *testing.T, a, b *linkedca.Webhook) { + assert.Equal(t, a.Name, b.Name) + assert.Equal(t, a.Url, b.Url) + assert.Equal(t, a.Kind, b.Kind) + assert.Equal(t, a.CertType, b.CertType) + assert.Equal(t, a.DisableTlsClientAuth, b.DisableTlsClientAuth) + + assert.Equal(t, a.GetAuth(), b.GetAuth()) +} + +func TestWebhookAdminResponder_CreateProvisionerWebhook(t *testing.T) { + type test struct { + auth adminAuthority + body []byte + ctx context.Context + err *admin.Error + response *linkedca.Webhook + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/existing-webhook": func(t *testing.T) test { + webhook := &linkedca.Webhook{ + Name: "already-exists", + Url: "https://example.com", + } + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{webhook}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewError(admin.ErrorConflictType, `provisioner "provName" already has a webhook with the name "already-exists"`) + err.Message = `provisioner "provName" already has a webhook with the name "already-exists"` + body := []byte(` + { + "name": "already-exists", + "url": "https://example.com", + "kind": "ENRICHING" + }`) + return test{ + ctx: ctx, + body: body, + err: err, + statusCode: 409, + } + }, + "fail/read.ProtoJSON": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/missing-name": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required") + adminErr.Message = "webhook name is required" + body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/missing-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid") + adminErr.Message = "webhook url is invalid" + body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/relative-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid") + adminErr.Message = "webhook url is invalid" + body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/http-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https") + adminErr.Message = "webhook url must use https" + body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/basic-auth-in-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password") + adminErr.Message = "webhook url may not contain username or password" + body := []byte(` + { + "name": "metadata", + "url": "https://user:pass@example.com", + "kind": "ENRICHING" + }`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/secret-in-request": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set") + adminErr.Message = "webhook secret must not be set" + body := []byte(` + { + "name": "metadata", + "url": "https://example.com", + "kind": "ENRICHING", + "secret": "secret" + }`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/auth.UpdateProvisioner-error": func(t *testing.T) test { + adm := &linkedca.Admin{ + Subject: "step", + } + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithAdmin(context.Background(), adm) + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error creating provisioner webhook: force") + adminErr.Message = "error creating provisioner webhook: force" + body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return &authority.PolicyError{ + Typ: authority.StoreFailure, + Err: errors.New("force"), + } + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING", "certType": "X509"}`) + return test{ + ctx: ctx, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + assert.Equal(t, linkedca.Webhook_X509, nu.Webhooks[0].CertType) + return nil + }, + }, + body: body, + response: &linkedca.Webhook{ + Name: "metadata", + Url: "https://example.com", + Kind: linkedca.Webhook_ENRICHING, + CertType: linkedca.Webhook_X509, + }, + statusCode: 201, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, &admin.MockDB{}) + war := NewWebhookAdminResponder() + + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(ctx) + w := httptest.NewRecorder() + + war.CreateProvisionerWebhook(w, req) + res := w.Result() + + assert.Equal(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.NoError(t, err) + + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(ae.Message, "syntax error")) + } else { + assert.Equal(t, tc.err.Message, ae.Message) + } + + return + } + + resp := &linkedca.Webhook{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, protojson.Unmarshal(body, resp)) + + assertEqualWebhook(t, tc.response, resp) + assert.NotEmpty(t, resp.Secret) + assert.NotEmpty(t, resp.Id) + }) + } +} + +func TestWebhookAdminResponder_DeleteProvisionerWebhook(t *testing.T) { + type test struct { + auth adminAuthority + err *admin.Error + statusCode int + provisionerWebhooks []*linkedca.Webhook + webhookName string + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.UpdateProvisioner-error": func(t *testing.T) test { + adminErr := admin.NewError(admin.ErrorServerInternalType, "error deleting provisioner webhook: force") + adminErr.Message = "error deleting provisioner webhook: force" + return test{ + err: adminErr, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return &authority.PolicyError{ + Typ: authority.StoreFailure, + Err: errors.New("force"), + } + }, + }, + statusCode: 500, + webhookName: "my-webhook", + provisionerWebhooks: []*linkedca.Webhook{ + {Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}, + }, + } + }, + "ok/not-found": func(t *testing.T) test { + return test{ + statusCode: 200, + webhookName: "no-exists", + provisionerWebhooks: nil, + } + }, + "ok": func(t *testing.T) test { + return test{ + statusCode: 200, + webhookName: "exists", + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + assert.Equal(t, nu.Webhooks, []*linkedca.Webhook{ + {Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}, + }) + return nil + }, + }, + provisionerWebhooks: []*linkedca.Webhook{ + {Name: "exists", Url: "https.example.com", Kind: linkedca.Webhook_ENRICHING}, + {Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}, + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + mockMustAuthority(t, tc.auth) + + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("webhookName", tc.webhookName) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: tc.provisionerWebhooks, + } + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + ctx = admin.NewContext(ctx, &admin.MockDB{}) + req := httptest.NewRequest("DELETE", "/foo", nil).WithContext(ctx) + + war := NewWebhookAdminResponder() + + w := httptest.NewRecorder() + + war.DeleteProvisionerWebhook(w, req) + res := w.Result() + + assert.Equal(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.NoError(t, err) + + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(ae.Message, "syntax error")) + } else { + assert.Equal(t, tc.err.Message, ae.Message) + } + + return + } + + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + res.Body.Close() + response := DeleteResponse{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equal(t, "ok", response.Status) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) + }) + } +} + +func TestWebhookAdminResponder_UpdateProvisionerWebhook(t *testing.T) { + type test struct { + auth adminAuthority + adminDB admin.DB + body []byte + ctx context.Context + err *admin.Error + response *linkedca.Webhook + statusCode int + } + var tests = map[string]func(t *testing.T) test{ + "fail/not-found": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "exists", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + err := admin.NewError(admin.ErrorNotFoundType, `provisioner "provName" has no webhook with the name "no-exists"`) + err.Message = `provisioner "provName" has no webhook with the name "no-exists"` + body := []byte(` + { + "name": "no-exists", + "url": "https://example.com", + "kind": "ENRICHING" + }`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + body: body, + err: err, + statusCode: 404, + } + }, + "fail/read.ProtoJSON": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?") + adminErr.Message = "proto: syntax error (line 1:2): invalid value ?" + body := []byte("{?}") + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/missing-name": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required") + adminErr.Message = "webhook name is required" + body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/missing-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid") + adminErr.Message = "webhook url is invalid" + body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/relative-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid") + adminErr.Message = "webhook url is invalid" + body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/http-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https") + adminErr.Message = "webhook url must use https" + body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/basic-auth-in-url": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password") + adminErr.Message = "webhook url may not contain username or password" + body := []byte(` + { + "name": "my-webhook", + "url": "https://user:pass@example.com", + "kind": "ENRICHING" + }`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/different-secret-in-request": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING, Secret: "c2VjcmV0"}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated") + adminErr.Message = "webhook secret cannot be updated" + body := []byte(` + { + "name": "my-webhook", + "url": "https://example.com", + "kind": "ENRICHING", + "secret": "secret" + }`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, + "fail/auth.UpdateProvisioner-error": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating provisioner webhook: force") + adminErr.Message = "error updating provisioner webhook: force" + body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return &authority.PolicyError{ + Typ: authority.StoreFailure, + Err: errors.New("force"), + } + }, + }, + body: body, + err: adminErr, + statusCode: 500, + } + }, + "ok": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}}, + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`) + return test{ + ctx: ctx, + adminDB: &admin.MockDB{}, + auth: &mockAdminAuthority{ + MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error { + return nil + }, + }, + body: body, + response: &linkedca.Webhook{ + Name: "my-webhook", + Url: "https://example.com", + Kind: linkedca.Webhook_ENRICHING, + }, + statusCode: 201, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + mockMustAuthority(t, tc.auth) + ctx := admin.NewContext(tc.ctx, tc.adminDB) + war := NewWebhookAdminResponder() + + req := httptest.NewRequest("PUT", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(ctx) + w := httptest.NewRecorder() + + war.UpdateProvisionerWebhook(w, req) + res := w.Result() + + assert.Equal(t, tc.statusCode, res.StatusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.NoError(t, err) + + ae := testAdminError{} + assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) + + assert.Equal(t, tc.err.Type, ae.Type) + assert.Equal(t, tc.err.StatusCode(), res.StatusCode) + assert.Equal(t, tc.err.Detail, ae.Detail) + assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) + + // when the error message starts with "proto", we expect it to have + // a syntax error (in the tests). If the message doesn't start with "proto", + // we expect a full string match. + if strings.HasPrefix(tc.err.Message, "proto:") { + assert.True(t, strings.Contains(ae.Message, "syntax error")) + } else { + assert.Equal(t, tc.err.Message, ae.Message) + } + + return + } + + resp := &linkedca.Webhook{} + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.NoError(t, protojson.Unmarshal(body, resp)) + + assertEqualWebhook(t, tc.response, resp) + }) + } +} diff --git a/authority/admin/db/nosql/provisioner.go b/authority/admin/db/nosql/provisioner.go index c82d4afe..da116e0b 100644 --- a/authority/admin/db/nosql/provisioner.go +++ b/authority/admin/db/nosql/provisioner.go @@ -24,6 +24,24 @@ type dbProvisioner struct { SSHTemplate *linkedca.Template `json:"sshTemplate"` CreatedAt time.Time `json:"createdAt"` DeletedAt time.Time `json:"deletedAt"` + Webhooks []dbWebhook `json:"webhooks,omitempty"` +} + +type dbBasicAuth struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type dbWebhook struct { + Name string `json:"name"` + ID string `json:"id"` + URL string `json:"url"` + Kind string `json:"kind"` + Secret string `json:"secret"` + BearerToken string `json:"bearerToken,omitempty"` + BasicAuth *dbBasicAuth `json:"basicAuth,omitempty"` + DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"` + CertType string `json:"certType,omitempty"` } func (dbp *dbProvisioner) clone() *dbProvisioner { @@ -48,6 +66,7 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) { SshTemplate: dbp.SSHTemplate, CreatedAt: timestamppb.New(dbp.CreatedAt), DeletedAt: timestamppb.New(dbp.DeletedAt), + Webhooks: dbWebhooksToLinkedca(dbp.Webhooks), }, nil } @@ -164,6 +183,7 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) X509Template: prov.X509Template, SSHTemplate: prov.SshTemplate, CreatedAt: clock.Now(), + Webhooks: linkedcaWebhooksToDB(prov.Webhooks), } if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil { @@ -193,6 +213,7 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) } nu.X509Template = prov.X509Template nu.SSHTemplate = prov.SshTemplate + nu.Webhooks = linkedcaWebhooksToDB(prov.Webhooks) return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable) } @@ -209,3 +230,70 @@ func (db *DB) DeleteProvisioner(ctx context.Context, id string) error { return db.save(ctx, old.ID, nu, old, "provisioner", provisionersTable) } + +func dbWebhooksToLinkedca(dbwhs []dbWebhook) []*linkedca.Webhook { + if len(dbwhs) == 0 { + return nil + } + lwhs := make([]*linkedca.Webhook, len(dbwhs)) + + for i, dbwh := range dbwhs { + lwh := &linkedca.Webhook{ + Name: dbwh.Name, + Id: dbwh.ID, + Url: dbwh.URL, + Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[dbwh.Kind]), + Secret: dbwh.Secret, + DisableTlsClientAuth: dbwh.DisableTLSClientAuth, + CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[dbwh.CertType]), + } + if dbwh.BearerToken != "" { + lwh.Auth = &linkedca.Webhook_BearerToken{ + BearerToken: &linkedca.BearerToken{ + BearerToken: dbwh.BearerToken, + }, + } + } else if dbwh.BasicAuth != nil && (dbwh.BasicAuth.Username != "" || dbwh.BasicAuth.Password != "") { + lwh.Auth = &linkedca.Webhook_BasicAuth{ + BasicAuth: &linkedca.BasicAuth{ + Username: dbwh.BasicAuth.Username, + Password: dbwh.BasicAuth.Password, + }, + } + } + lwhs[i] = lwh + } + + return lwhs +} + +func linkedcaWebhooksToDB(lwhs []*linkedca.Webhook) []dbWebhook { + if len(lwhs) == 0 { + return nil + } + dbwhs := make([]dbWebhook, len(lwhs)) + + for i, lwh := range lwhs { + dbwh := dbWebhook{ + Name: lwh.Name, + ID: lwh.Id, + URL: lwh.Url, + Kind: lwh.Kind.String(), + Secret: lwh.Secret, + DisableTLSClientAuth: lwh.DisableTlsClientAuth, + CertType: lwh.CertType.String(), + } + switch a := lwh.GetAuth().(type) { + case *linkedca.Webhook_BearerToken: + dbwh.BearerToken = a.BearerToken.BearerToken + case *linkedca.Webhook_BasicAuth: + dbwh.BasicAuth = &dbBasicAuth{ + Username: a.BasicAuth.Username, + Password: a.BasicAuth.Password, + } + } + dbwhs[i] = dbwh + } + + return dbwhs +} diff --git a/authority/admin/db/nosql/provisioner_test.go b/authority/admin/db/nosql/provisioner_test.go index c5caf696..8aa58d49 100644 --- a/authority/admin/db/nosql/provisioner_test.go +++ b/authority/admin/db/nosql/provisioner_test.go @@ -137,6 +137,7 @@ func TestDB_getDBProvisioner(t *testing.T) { } }, "fail/deleted": func(t *testing.T) test { + now := clock.Now() dbp := &dbProvisioner{ ID: provID, @@ -210,6 +211,7 @@ func TestDB_getDBProvisioner(t *testing.T) { assert.Equals(t, dbp.Name, tc.dbp.Name) assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) assert.Fatal(t, dbp.DeletedAt.IsZero()) + assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks) } }) } @@ -300,6 +302,7 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) { assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate) assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) assert.Fatal(t, dbp.DeletedAt.IsZero()) + assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks) } }) } @@ -353,6 +356,15 @@ func defaultDBP(t *testing.T) *dbProvisioner { Data: []byte("zap"), }, CreatedAt: clock.Now(), + Webhooks: []dbWebhook{ + { + Name: "metadata", + URL: "https://inventory.smallstep.com", + Kind: linkedca.Webhook_ENRICHING.String(), + Secret: "secret", + BearerToken: "token", + }, + }, } } @@ -419,6 +431,7 @@ func TestDB_unmarshalProvisioner(t *testing.T) { assert.Equals(t, prov.Claims, tc.dbp.Claims) assert.Equals(t, prov.X509Template, tc.dbp.X509Template) assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) + assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks)) retDetailsBytes, err := json.Marshal(prov.Details.GetData()) assert.FatalError(t, err) @@ -557,6 +570,7 @@ func TestDB_GetProvisioner(t *testing.T) { assert.Equals(t, prov.Claims, tc.dbp.Claims) assert.Equals(t, prov.X509Template, tc.dbp.X509Template) assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) + assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks)) retDetailsBytes, err := json.Marshal(prov.Details.GetData()) assert.FatalError(t, err) @@ -629,6 +643,7 @@ func TestDB_DeleteProvisioner(t *testing.T) { assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate) assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt) assert.Equals(t, _dbp.Details, dbp.Details) + assert.Equals(t, _dbp.Webhooks, dbp.Webhooks) assert.True(t, _dbp.DeletedAt.Before(time.Now())) assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute))) @@ -668,6 +683,7 @@ func TestDB_DeleteProvisioner(t *testing.T) { assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate) assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt) assert.Equals(t, _dbp.Details, dbp.Details) + assert.Equals(t, _dbp.Webhooks, dbp.Webhooks) assert.True(t, _dbp.DeletedAt.Before(time.Now())) assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute))) @@ -819,6 +835,7 @@ func TestDB_GetProvisioners(t *testing.T) { assert.Equals(t, provs[0].Claims, fooProv.Claims) assert.Equals(t, provs[0].X509Template, fooProv.X509Template) assert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate) + assert.Equals(t, provs[0].Webhooks, dbWebhooksToLinkedca(fooProv.Webhooks)) retDetailsBytes, err := json.Marshal(provs[0].Details.GetData()) assert.FatalError(t, err) @@ -831,6 +848,7 @@ func TestDB_GetProvisioners(t *testing.T) { assert.Equals(t, provs[1].Claims, zapProv.Claims) assert.Equals(t, provs[1].X509Template, zapProv.X509Template) assert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate) + assert.Equals(t, provs[1].Webhooks, dbWebhooksToLinkedca(zapProv.Webhooks)) retDetailsBytes, err = json.Marshal(provs[1].Details.GetData()) assert.FatalError(t, err) @@ -895,6 +913,7 @@ func TestDB_CreateProvisioner(t *testing.T) { assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) + assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks)) retDetailsBytes, err := json.Marshal(prov.Details.GetData()) assert.FatalError(t, err) @@ -932,6 +951,7 @@ func TestDB_CreateProvisioner(t *testing.T) { assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) + assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks)) retDetailsBytes, err := json.Marshal(prov.Details.GetData()) assert.FatalError(t, err) @@ -1080,6 +1100,7 @@ func TestDB_UpdateProvisioner(t *testing.T) { assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) + assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks)) retDetailsBytes, err := json.Marshal(prov.Details.GetData()) assert.FatalError(t, err) @@ -1141,6 +1162,12 @@ func TestDB_UpdateProvisioner(t *testing.T) { }, }, } + prov.Webhooks = []*linkedca.Webhook{ + { + Name: "users", + Url: "https://example.com/users", + }, + } data, err := json.Marshal(dbp) assert.FatalError(t, err) @@ -1168,6 +1195,7 @@ func TestDB_UpdateProvisioner(t *testing.T) { assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) + assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks)) retDetailsBytes, err := json.Marshal(prov.Details.GetData()) assert.FatalError(t, err) @@ -1206,3 +1234,164 @@ func TestDB_UpdateProvisioner(t *testing.T) { }) } } + +func Test_linkedcaWebhooksToDB(t *testing.T) { + type test struct { + in []*linkedca.Webhook + want []dbWebhook + } + var tests = map[string]test{ + "nil": { + in: nil, + want: nil, + }, + "zero": { + in: []*linkedca.Webhook{}, + want: nil, + }, + "bearer": { + in: []*linkedca.Webhook{ + { + Name: "bearer", + Url: "https://example.com", + Kind: linkedca.Webhook_ENRICHING, + Secret: "secret", + Auth: &linkedca.Webhook_BearerToken{ + BearerToken: &linkedca.BearerToken{ + BearerToken: "token", + }, + }, + DisableTlsClientAuth: true, + CertType: linkedca.Webhook_X509, + }, + }, + want: []dbWebhook{ + { + Name: "bearer", + URL: "https://example.com", + Kind: "ENRICHING", + Secret: "secret", + BearerToken: "token", + DisableTLSClientAuth: true, + CertType: linkedca.Webhook_X509.String(), + }, + }, + }, + "basic": { + in: []*linkedca.Webhook{ + { + Name: "basic", + Url: "https://example.com", + Kind: linkedca.Webhook_ENRICHING, + Secret: "secret", + Auth: &linkedca.Webhook_BasicAuth{ + BasicAuth: &linkedca.BasicAuth{ + Username: "user", + Password: "pass", + }, + }, + }, + }, + want: []dbWebhook{ + { + Name: "basic", + URL: "https://example.com", + Kind: "ENRICHING", + Secret: "secret", + BasicAuth: &dbBasicAuth{ + Username: "user", + Password: "pass", + }, + CertType: linkedca.Webhook_ALL.String(), + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := linkedcaWebhooksToDB(tc.in) + assert.Equals(t, tc.want, got) + }) + } +} + +func Test_dbWebhooksToLinkedca(t *testing.T) { + type test struct { + in []dbWebhook + want []*linkedca.Webhook + } + var tests = map[string]test{ + "nil": { + in: nil, + want: nil, + }, + "zero": { + in: []dbWebhook{}, + want: nil, + }, + "bearer": { + in: []dbWebhook{ + { + Name: "bearer", + ID: "69350cb6-6c31-4b5e-bf25-affd5053427d", + URL: "https://example.com", + Kind: "ENRICHING", + Secret: "secret", + BearerToken: "token", + DisableTLSClientAuth: true, + }, + }, + want: []*linkedca.Webhook{ + { + Name: "bearer", + Id: "69350cb6-6c31-4b5e-bf25-affd5053427d", + Url: "https://example.com", + Kind: linkedca.Webhook_ENRICHING, + Secret: "secret", + Auth: &linkedca.Webhook_BearerToken{ + BearerToken: &linkedca.BearerToken{ + BearerToken: "token", + }, + }, + DisableTlsClientAuth: true, + }, + }, + }, + "basic": { + in: []dbWebhook{ + { + Name: "basic", + ID: "69350cb6-6c31-4b5e-bf25-affd5053427d", + URL: "https://example.com", + Kind: "ENRICHING", + Secret: "secret", + BasicAuth: &dbBasicAuth{ + Username: "user", + Password: "pass", + }, + }, + }, + want: []*linkedca.Webhook{ + { + Name: "basic", + Id: "69350cb6-6c31-4b5e-bf25-affd5053427d", + Url: "https://example.com", + Kind: linkedca.Webhook_ENRICHING, + Secret: "secret", + Auth: &linkedca.Webhook_BasicAuth{ + BasicAuth: &linkedca.BasicAuth{ + Username: "user", + Password: "pass", + }, + }, + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := dbWebhooksToLinkedca(tc.in) + assert.Equals(t, tc.want, got) + }) + } +} diff --git a/authority/authority.go b/authority/authority.go index 43216f1e..deebdaa9 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,6 +8,7 @@ import ( "crypto/x509" "encoding/hex" "log" + "net/http" "strings" "sync" "time" @@ -46,6 +47,7 @@ type Authority struct { adminDB admin.DB templates *templates.Templates linkedCAToken string + webhookClient *http.Client // X509 CA password []byte @@ -76,7 +78,7 @@ type Authority struct { crlStopper chan struct{} crlMutex sync.Mutex - // Do not re-initialize + // If true, do not re-initialize initOnce bool startTime time.Time @@ -94,8 +96,11 @@ type Authority struct { adminMutex sync.RWMutex - // Do Not initialize the authority + // If true, do not initialize the authority skipInit bool + + // If true, do not output initialization logs + quietInit bool } // Info contains information about the authority. @@ -603,20 +608,74 @@ func (a *Authority) init() error { return admin.WrapErrorISE(err, "error loading provisioners to initialize authority") } if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") { - // Create First Provisioner - prov, err := CreateFirstProvisioner(ctx, a.adminDB, string(a.password)) - if err != nil { - return admin.WrapErrorISE(err, "error creating first provisioner") + // Migration will currently only be kicked off once, because either one or more provisioners + // are migrated or a default JWK provisioner will be created in the DB. It won't run for + // linked or hosted deployments. Not for linked, because that case is explicitly checked + // for above. Not for hosted, because there'll be at least an existing OIDC provisioner. + var firstJWKProvisioner *linkedca.Provisioner + if len(a.config.AuthorityConfig.Provisioners) > 0 { + // Existing provisioners detected; try migrating them to DB storage. + a.initLogf("Starting migration of provisioners") + for _, p := range a.config.AuthorityConfig.Provisioners { + lp, err := ProvisionerToLinkedca(p) + if err != nil { + return admin.WrapErrorISE(err, "error transforming provisioner %q while migrating", p.GetName()) + } + + // Store the provisioner to be migrated + if err := a.adminDB.CreateProvisioner(ctx, lp); err != nil { + return admin.WrapErrorISE(err, "error creating provisioner %q while migrating", p.GetName()) + } + + // Mark the first JWK provisioner, so that it can be used for administration purposes + if firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK { + firstJWKProvisioner = lp + a.initLogf("Migrated JWK provisioner %q with admin permissions", p.GetName()) + } else { + a.initLogf("Migrated %s provisioner %q", p.GetType(), p.GetName()) + } + } + + c := a.config + if c.WasLoadedFromFile() { + // The provisioners in the configuration file can be deleted from + // the file by editing it. Automatic rewriting of the file was considered + // to be too surprising for users and not the right solution for all + // use cases, so we leave it up to users to this themselves. + a.initLogf("Provisioners that were migrated can now be removed from `ca.json` by editing it") + } + + a.initLogf("Finished migrating provisioners") } - // Create first admin + // Create first JWK provisioner for remote administration purposes if none exists yet + if firstJWKProvisioner == nil { + firstJWKProvisioner, err = CreateFirstProvisioner(ctx, a.adminDB, string(a.password)) + if err != nil { + return admin.WrapErrorISE(err, "error creating first provisioner") + } + a.initLogf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName()) + } + + // Create first super admin, belonging to the first JWK provisioner + // TODO(hs): pass a user-provided first super admin subject to here. With `ca init` it's + // added to the DB immediately if using remote management. But when migrating from + // ca.json to the DB, this option doesn't exist. Adding a flag just to do it during + // migration isn't nice. We could opt for a user to change it afterwards. There exist + // cases in which creation of `step` could lock out a user from API access. This is the + // case if `step` isn't allowed to be signed by Name Constraints or the X.509 policy. + // We have protection for that when creating and updating a policy, but if a policy or + // Name Constraints are in use at the time of migration, that could lock the user out. + superAdminSubject := "step" if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{ - ProvisionerId: prov.Id, - Subject: "step", + ProvisionerId: firstJWKProvisioner.Id, + Subject: superAdminSubject, Type: linkedca.Admin_SUPER_ADMIN, }); err != nil { return admin.WrapErrorISE(err, "error creating first admin") } + + a.initLogf("Created super admin %q for JWK provisioner %q", superAdminSubject, firstJWKProvisioner.GetName()) } } @@ -678,6 +737,14 @@ func (a *Authority) init() error { return nil } +// initLogf is used to log initialization information. The output +// can be disabled by starting the CA with the `--quiet` flag. +func (a *Authority) initLogf(format string, v ...any) { + if !a.quietInit { + log.Printf(format, v...) + } +} + // GetID returns the define authority id or a zero uuid. func (a *Authority) GetID() string { const zeroUUID = "00000000-0000-0000-0000-000000000000" diff --git a/authority/authorize_test.go b/authority/authorize_test.go index 8f49cf03..7dc22f3a 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -491,7 +491,7 @@ func TestAuthority_authorizeSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Equals(t, 9, len(got)) // number of provisioner.SignOptions returned + assert.Equals(t, 10, len(got)) // number of provisioner.SignOptions returned } } }) @@ -1034,7 +1034,7 @@ func TestAuthority_authorizeSSHSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) { - assert.Len(t, 9, got) // number of provisioner.SignOptions returned + assert.Len(t, 10, got) // number of provisioner.SignOptions returned } } }) diff --git a/authority/config/config.go b/authority/config/config.go index 79ac3e88..f58a3354 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -79,6 +79,9 @@ type Config struct { CommonName string `json:"commonName,omitempty"` CRL *CRLConfig `json:"crl,omitempty"` SkipValidation bool `json:"-"` + + // Keeps record of the filename the Config is read from + loadedFromFilepath string } // CRLConfig represents config options for CRL generation @@ -218,6 +221,10 @@ func LoadConfiguration(filename string) (*Config, error) { return nil, errors.Wrapf(err, "error parsing %s", filename) } + // store filename that was read to populate Config + c.loadedFromFilepath = filename + + // initialize the Config c.Init() return &c, nil @@ -257,6 +264,30 @@ func (c *Config) Save(filename string) error { return errors.Wrapf(enc.Encode(c), "error writing %s", filename) } +// Commit saves the current configuration to the same +// file it was initially loaded from. +// +// TODO(hs): rename Save() to WriteTo() and replace this +// with Save()? Or is Commit clear enough. +func (c *Config) Commit() error { + if !c.WasLoadedFromFile() { + return errors.New("cannot commit configuration if not loaded from file") + } + return c.Save(c.loadedFromFilepath) +} + +// WasLoadedFromFile returns whether or not the Config was +// loaded from a file. +func (c *Config) WasLoadedFromFile() bool { + return c.loadedFromFilepath != "" +} + +// Filepath returns the path to the file the Config was +// loaded from. +func (c *Config) Filepath() string { + return c.loadedFromFilepath +} + // Validate validates the configuration. func (c *Config) Validate() error { switch { diff --git a/authority/linkedca.go b/authority/linkedca.go index 7179b1d7..7a950c02 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -278,6 +278,7 @@ func (c *linkedCaClient) StoreCertificateChain(p provisioner.Interface, fullchai PemCertificate: serializeCertificateChain(fullchain[0]), PemCertificateChain: serializeCertificateChain(fullchain[1:]...), Provisioner: createProvisionerIdentity(p), + AttestationData: createAttestationData(p), RaProvisioner: raProvisioner, EndpointId: endpointID, }) @@ -395,26 +396,34 @@ func createProvisionerIdentity(p provisioner.Interface) *linkedca.ProvisionerIde } } -type raProvisioner interface { - RAInfo() *provisioner.RAInfo -} - func createRegistrationAuthorityProvisioner(p provisioner.Interface) (*linkedca.RegistrationAuthorityProvisioner, string) { if rap, ok := p.(raProvisioner); ok { - info := rap.RAInfo() - typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)] - return &linkedca.RegistrationAuthorityProvisioner{ - AuthorityId: info.AuthorityID, - Provisioner: &linkedca.ProvisionerIdentity{ - Id: info.ProvisionerID, - Type: linkedca.Provisioner_Type(typ), - Name: info.ProvisionerName, - }, - }, info.EndpointID + if info := rap.RAInfo(); info != nil { + typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)] + return &linkedca.RegistrationAuthorityProvisioner{ + AuthorityId: info.AuthorityID, + Provisioner: &linkedca.ProvisionerIdentity{ + Id: info.ProvisionerID, + Type: linkedca.Provisioner_Type(typ), + Name: info.ProvisionerName, + }, + }, info.EndpointID + } } return nil, "" } +func createAttestationData(p provisioner.Interface) *linkedca.AttestationData { + if ap, ok := p.(attProvisioner); ok { + if data := ap.AttestationData(); data != nil { + return &linkedca.AttestationData{ + PermanentIdentifier: data.PermanentIdentifier, + } + } + } + return nil +} + func serializeCertificate(crt *x509.Certificate) string { if crt == nil { return "" diff --git a/authority/options.go b/authority/options.go index 8e1a01ff..bf443ed6 100644 --- a/authority/options.go +++ b/authority/options.go @@ -5,6 +5,7 @@ import ( "crypto" "crypto/x509" "encoding/pem" + "net/http" "github.com/pkg/errors" "golang.org/x/crypto/ssh" @@ -85,6 +86,22 @@ func WithDatabase(d db.AuthDB) Option { } } +// WithQuietInit disables log output when the authority is initialized. +func WithQuietInit() Option { + return func(a *Authority) error { + a.quietInit = true + return nil + } +} + +// WithWebhookClient sets the http.Client to be used for outbound requests. +func WithWebhookClient(c *http.Client) Option { + return func(a *Authority) error { + a.webhookClient = c + return nil + } +} + // WithGetIdentityFunc sets a custom function to retrieve the identity from // an external resource. func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option { diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 9a5e9f1c..67a24919 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -10,6 +10,7 @@ import ( "time" "github.com/pkg/errors" + "go.step.sm/linkedca" ) // ACMEChallenge represents the supported acme challenges. @@ -252,22 +253,22 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), + p.ctl.newWebhookController(nil, linkedca.Webhook_X509), } return opts, nil } // AuthorizeRevoke is called just before the certificate is to be revoked by -// the CA. It can be used to authorize revocation of a certificate. It -// currently is a no-op. -// TODO(hs): add configuration option that toggles revocation? Or change function signature to make it more useful? -// Or move certain logic out of the Revoke API to here? Would likely involve some more stuff in the ctx. +// the CA. It can be used to authorize revocation of a certificate. With the +// ACME protocol, revocation authorization is specified and performed as part +// of the client/server interaction, so this is a no-op. func (p *ACME) AuthorizeRevoke(ctx context.Context, token string) error { return nil } // AuthorizeRenew returns an error if the renewal is disabled. -// NOTE: This method does not actually validate the certificate or check it's +// NOTE: This method does not actually validate the certificate or check its // revocation status. Just confirms that the provisioner that created the // certificate was configured to allow renewals. func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error { diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index d476ef08..94684ce1 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -269,7 +269,7 @@ func TestACME_AuthorizeSign(t *testing.T) { } } else { if assert.Nil(t, tc.err) && assert.NotNil(t, opts) { - assert.Equals(t, 7, len(opts)) // number of SignOptions returned + assert.Equals(t, 8, len(opts)) // number of SignOptions returned for _, o := range opts { switch v := o.(type) { case *ACME: @@ -288,6 +288,8 @@ func TestACME_AuthorizeSign(t *testing.T) { assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration()) case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index caf72142..0560877c 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -21,6 +21,7 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -484,6 +485,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er 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), ), nil } @@ -765,5 +767,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertDefaultValidator{}, // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), + // Call webhooks + p.ctl.newWebhookController(data, linkedca.Webhook_SSH), ), nil } diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 0ad1eca9..668bc13b 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -642,11 +642,11 @@ func TestAWS_AuthorizeSign(t *testing.T) { code int wantErr bool }{ - {"ok", p1, args{t1, "foo.local"}, 8, http.StatusOK, false}, - {"ok", p2, args{t2, "instance-id"}, 12, http.StatusOK, false}, - {"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 12, http.StatusOK, false}, - {"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 12, http.StatusOK, false}, - {"ok", p1, args{t4, "instance-id"}, 8, http.StatusOK, false}, + {"ok", p1, args{t1, "foo.local"}, 9, http.StatusOK, false}, + {"ok", p2, args{t2, "instance-id"}, 13, http.StatusOK, false}, + {"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 13, http.StatusOK, false}, + {"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 13, http.StatusOK, false}, + {"ok", p1, args{t4, "instance-id"}, 9, http.StatusOK, false}, {"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true}, {"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true}, {"fail subject", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true}, @@ -701,6 +701,8 @@ func TestAWS_AuthorizeSign(t *testing.T) { assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"}) case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index d6f71a89..4b161d9c 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -17,6 +17,7 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -363,6 +364,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), + p.ctl.newWebhookController(data, linkedca.Webhook_X509), ), nil } @@ -431,6 +433,8 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio &sshCertDefaultValidator{}, // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), + // Call webhooks + p.ctl.newWebhookController(data, linkedca.Webhook_SSH), ), nil } diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index 539a9d16..84f2ebbf 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -474,11 +474,11 @@ func TestAzure_AuthorizeSign(t *testing.T) { code int wantErr bool }{ - {"ok", p1, args{t1}, 7, http.StatusOK, false}, - {"ok", p2, args{t2}, 12, http.StatusOK, false}, - {"ok", p1, args{t11}, 7, http.StatusOK, false}, - {"ok", p5, args{t5}, 7, http.StatusOK, false}, - {"ok", p7, args{t7}, 7, http.StatusOK, false}, + {"ok", p1, args{t1}, 8, http.StatusOK, false}, + {"ok", p2, args{t2}, 13, http.StatusOK, false}, + {"ok", p1, args{t11}, 8, http.StatusOK, false}, + {"ok", p5, args{t5}, 8, http.StatusOK, false}, + {"ok", p7, args{t7}, 8, http.StatusOK, false}, {"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true}, {"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true}, {"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true}, @@ -530,6 +530,8 @@ func TestAzure_AuthorizeSign(t *testing.T) { assert.Equals(t, []string(v), []string{"virtualMachine"}) case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index 063ab50c..ef96639f 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "go.step.sm/linkedca" "golang.org/x/crypto/ssh" ) @@ -23,6 +24,8 @@ type Controller struct { AuthorizeRenewFunc AuthorizeRenewFunc AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc policy *policyEngine + webhookClient *http.Client + webhooks []*Webhook } // NewController initializes a new provisioner controller. @@ -43,6 +46,8 @@ func NewController(p Interface, claims *Claims, config Config, options *Options) AuthorizeRenewFunc: config.AuthorizeRenewFunc, AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc, policy: policy, + webhookClient: config.WebhookClient, + webhooks: options.GetWebhooks(), }, nil } @@ -72,6 +77,19 @@ 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 { + client := c.webhookClient + if client == nil { + client = http.DefaultClient + } + return &WebhookController{ + TemplateData: templateData, + client: client, + webhooks: c.webhooks, + certType: certType, + } +} + // Identity is the type representing an externally supplied identity that is used // by provisioners to populate certificate fields. type Identity struct { diff --git a/authority/provisioner/controller_test.go b/authority/provisioner/controller_test.go index 37cbfd89..c628f074 100644 --- a/authority/provisioner/controller_test.go +++ b/authority/provisioner/controller_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "golang.org/x/crypto/ssh" "github.com/smallstep/certificates/authority/policy" @@ -445,3 +447,18 @@ 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") + } + if ctl.certType != linkedca.Webhook_X509 { + t.Error("Failed to set certType") + } + if ctl.client == nil { + t.Error("Failed to set client") + } +} diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 19b731fa..e9b372b2 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -18,6 +18,7 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -272,6 +273,7 @@ func (p *GCP) AuthorizeSign(ctx 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), ), nil } @@ -437,5 +439,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertDefaultValidator{}, // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), + // Call webhooks + p.ctl.newWebhookController(data, linkedca.Webhook_SSH), ), nil } diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index 0aa12301..7705b44a 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -516,9 +516,9 @@ func TestGCP_AuthorizeSign(t *testing.T) { code int wantErr bool }{ - {"ok", p1, args{t1}, 7, http.StatusOK, false}, - {"ok", p2, args{t2}, 12, http.StatusOK, false}, - {"ok", p3, args{t3}, 7, http.StatusOK, false}, + {"ok", p1, args{t1}, 8, http.StatusOK, false}, + {"ok", p2, args{t2}, 13, http.StatusOK, false}, + {"ok", p3, args{t3}, 8, http.StatusOK, false}, {"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true}, {"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true}, {"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true}, @@ -573,6 +573,8 @@ func TestGCP_AuthorizeSign(t *testing.T) { assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}) case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 5cfb0409..59332996 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -11,6 +11,7 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -194,6 +195,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er defaultSANsValidator(claims.SANs), newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), + p.ctl.newWebhookController(data, linkedca.Webhook_X509), }, nil } @@ -278,6 +280,8 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertDefaultValidator{}, // 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), ), nil } diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index c34ce918..19cee4fb 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -297,7 +297,7 @@ func TestJWK_AuthorizeSign(t *testing.T) { } } else { if assert.NotNil(t, got) { - assert.Equals(t, 9, len(got)) + assert.Equals(t, 10, len(got)) for _, o := range got { switch v := o.(type) { case *JWK: @@ -319,6 +319,7 @@ func TestJWK_AuthorizeSign(t *testing.T) { assert.Equals(t, []string(v), tt.sans) case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/k8sSA.go b/authority/provisioner/k8sSA.go index 3d79933a..e970616d 100644 --- a/authority/provisioner/k8sSA.go +++ b/authority/provisioner/k8sSA.go @@ -15,6 +15,7 @@ import ( "go.step.sm/crypto/pemutil" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -242,6 +243,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), + p.ctl.newWebhookController(data, linkedca.Webhook_X509), }, nil } @@ -287,6 +289,8 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio &sshCertDefaultValidator{}, // 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), ), nil } diff --git a/authority/provisioner/k8sSA_test.go b/authority/provisioner/k8sSA_test.go index 8cf06e53..48581c2d 100644 --- a/authority/provisioner/k8sSA_test.go +++ b/authority/provisioner/k8sSA_test.go @@ -297,11 +297,13 @@ func TestK8sSA_AuthorizeSign(t *testing.T) { assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration()) case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } } - assert.Equals(t, 7, len(opts)) + assert.Equals(t, 8, len(opts)) } } } @@ -368,7 +370,7 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) { } else { if assert.Nil(t, tc.err) { if assert.NotNil(t, opts) { - assert.Len(t, 8, opts) + assert.Len(t, 9, opts) for _, o := range opts { switch v := o.(type) { case Interface: @@ -384,6 +386,8 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) { case *sshNamePolicyValidator: assert.Equals(t, nil, v.userPolicyEngine) assert.Equals(t, nil, v.hostPolicyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index cde5857c..02762a0a 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -15,6 +15,7 @@ import ( "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x25519" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "golang.org/x/crypto/ssh" "github.com/smallstep/certificates/errs" @@ -164,6 +165,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption, defaultPublicKeyValidator{}, newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(p.ctl.getPolicy().getX509()), + p.ctl.newWebhookController(data, linkedca.Webhook_X509), }, nil } @@ -262,6 +264,8 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti &sshCertDefaultValidator{}, // Ensure that all principal names are allowed newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil), + // Call webhooks + p.ctl.newWebhookController(data, linkedca.Webhook_SSH), ), nil } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 5463f20c..3840a4a8 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -16,6 +16,7 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -356,6 +357,8 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e defaultPublicKeyValidator{}, newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(o.ctl.getPolicy().getX509()), + // webhooks + o.ctl.newWebhookController(data, linkedca.Webhook_X509), }, nil } @@ -460,6 +463,8 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption &sshCertDefaultValidator{}, // Ensure that all principal names are allowed newSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()), + // Call webhooks + o.ctl.newWebhookController(data, linkedca.Webhook_SSH), ), nil } diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index 3aa969e3..083799f6 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -323,7 +323,7 @@ func TestOIDC_AuthorizeSign(t *testing.T) { assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) } else if assert.NotNil(t, got) { - assert.Equals(t, 7, len(got)) + assert.Equals(t, 8, len(got)) for _, o := range got { switch v := o.(type) { case *OIDC: @@ -343,6 +343,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) { assert.Equals(t, string(v), "name@smallstep.com") case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index f5c919b4..702666a4 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -29,6 +29,9 @@ func (fn certificateOptionsFunc) Options(so SignOptions) []x509util.Option { type Options struct { X509 *X509Options `json:"x509,omitempty"` SSH *SSHOptions `json:"ssh,omitempty"` + + // Webhooks is a list of webhooks that can augment template data + Webhooks []*Webhook `json:"webhooks,omitempty"` } // GetX509Options returns the X.509 options. @@ -47,6 +50,14 @@ func (o *Options) GetSSHOptions() *SSHOptions { return o.SSH } +// GetWebhooks returns the webhooks options. +func (o *Options) GetWebhooks() []*Webhook { + if o == nil { + return nil + } + return o.Webhooks +} + // X509Options contains specific options for X.509 certificates. type X509Options struct { // Template contains a X.509 certificate template. It can be a JSON template diff --git a/authority/provisioner/options_test.go b/authority/provisioner/options_test.go index aaf4b36c..405ec8b7 100644 --- a/authority/provisioner/options_test.go +++ b/authority/provisioner/options_test.go @@ -68,6 +68,36 @@ func TestOptions_GetSSHOptions(t *testing.T) { } } +func TestOptions_GetWebhooks(t *testing.T) { + type fields struct { + o *Options + } + tests := []struct { + name string + fields fields + want []*Webhook + }{ + {"ok", fields{&Options{Webhooks: []*Webhook{ + {Name: "foo"}, + {Name: "bar"}, + }}}, + []*Webhook{ + {Name: "foo"}, + {Name: "bar"}, + }, + }, + {"nil", fields{&Options{}}, nil}, + {"nilOptions", fields{nil}, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.fields.o.GetWebhooks(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Options.GetWebhooks() = %v, want %v", got, tt.want) + } + }) + } +} + func TestProvisionerX509Options_HasTemplate(t *testing.T) { type fields struct { Template string diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 29d44c1c..9d65d585 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/json" stderrors "errors" + "net/http" "net/url" "strings" @@ -222,6 +223,8 @@ type Config struct { // AuthorizeSSHRenewFunc is a function that returns nil if a given SSH // certificate can be renewed. AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc + // WebhookClient is an http client to use in webhook request + WebhookClient *http.Client } type provisioner struct { diff --git a/authority/provisioner/provisioner_test.go b/authority/provisioner/provisioner_test.go index 9678a20b..65fb8e1d 100644 --- a/authority/provisioner/provisioner_test.go +++ b/authority/provisioner/provisioner_test.go @@ -241,9 +241,10 @@ func TestUnimplementedMethods(t *testing.T) { default: t.Errorf("unexpected method %s", tt.method) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), http.StatusUnauthorized) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), http.StatusUnauthorized) + } assert.Equals(t, err.Error(), msg) }) } diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index c49c993e..0f27b206 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pkg/errors" + "go.step.sm/linkedca" ) // SCEP is the SCEP provisioner type, an entity that can authorize the @@ -128,6 +129,7 @@ func (s *SCEP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength), newValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()), newX509NamePolicyValidator(s.ctl.getPolicy().getX509()), + s.ctl.newWebhookController(nil, linkedca.Webhook_X509), }, nil } diff --git a/authority/provisioner/ssh_test.go b/authority/provisioner/ssh_test.go index 3fd97f9d..6ad71459 100644 --- a/authority/provisioner/ssh_test.go +++ b/authority/provisioner/ssh_test.go @@ -69,6 +69,8 @@ func signSSHCertificate(key crypto.PublicKey, opts SignSSHOptions, signOpts []Si if err := o.Valid(opts); err != nil { return nil, err } + // call webhooks + case *WebhookController: default: return nil, fmt.Errorf("signSSH: invalid extra option type %T", o) } diff --git a/authority/provisioner/sshpop_test.go b/authority/provisioner/sshpop_test.go index 1e026883..ae75b349 100644 --- a/authority/provisioner/sshpop_test.go +++ b/authority/provisioner/sshpop_test.go @@ -218,9 +218,10 @@ func TestSSHPOP_authorizeToken(t *testing.T) { t.Run(name, func(t *testing.T) { tc := tt(t) if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign, true); err != nil { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -289,9 +290,10 @@ func TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) { t.Run(name, func(t *testing.T) { tc := tt(t) if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -370,9 +372,10 @@ func TestSSHPOP_AuthorizeSSHRenew(t *testing.T) { tc := tt(t) if cert, err := tc.p.AuthorizeSSHRenew(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -452,9 +455,10 @@ func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) { tc := tt(t) if cert, opts, err := tc.p.AuthorizeSSHRekey(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { diff --git a/authority/provisioner/testdata/certs/foo.crt b/authority/provisioner/testdata/certs/foo.crt new file mode 100644 index 00000000..eb06f218 --- /dev/null +++ b/authority/provisioner/testdata/certs/foo.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICIDCCAcagAwIBAgIQTL7pKDl8mFzRziotXbgjEjAKBggqhkjOPQQDAjAnMSUw +IwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIy +MjkyOVoXDTE5MDMyMzIyMjkyOVowHDEaMBgGA1UEAxMRZm9vLnNtYWxsc3RlcC5j +b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbptfDonFaeUPiTr52wl9r3dcz +greolwDRmsgyFgnr1EuKH56WRcgH1gjfL0pybFlO3PdgBukR4u+sveq343OAo4He +MIHbMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH +AwIwHQYDVR0OBBYEFP9pHiVlsx5mr4L2QirOb1G9Mo4jMB8GA1UdIwQYMBaAFKEe +9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWZvby5zbWFsbHN0ZXAuY29t +MEwGDCsGAQQBgqRkxihAAQQ8MDoCAQEECHN0ZXAtY2xpBCs0VUVMSng4ZTBhUzlt +MENIM2ZaMEVCN0Q1YVVQSUNiNzU5ekFMSEZlanZjMAoGCCqGSM49BAMCA0gAMEUC +IDxtNo1BX/4Sbf/+k1n+v//kh8ETr3clPvhjcyfvBIGTAiEAiT0kvbkPdCCnmHIw +lhpgBwT5YReZzBwIYXyKyJXc07M= +-----END CERTIFICATE----- diff --git a/authority/provisioner/testdata/secrets/foo.key b/authority/provisioner/testdata/secrets/foo.key new file mode 100644 index 00000000..b1b63324 --- /dev/null +++ b/authority/provisioner/testdata/secrets/foo.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJmnxm3N/ahRA2PWeZhRGJUKPU1lI44WcE4P1bynIim6oAoGCCqGSM49 +AwEHoUQDQgAEG6bXw6JxWnlD4k6+dsJfa93XM4K3qJcA0ZrIMhYJ69RLih+elkXI +B9YI3y9KcmxZTtz3YAbpEeLvrL3qt+NzgA== +-----END EC PRIVATE KEY----- diff --git a/authority/provisioner/webhook.go b/authority/provisioner/webhook.go new file mode 100644 index 00000000..ea02da35 --- /dev/null +++ b/authority/provisioner/webhook.go @@ -0,0 +1,209 @@ +package provisioner + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net/http" + "text/template" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/templates" + "github.com/smallstep/certificates/webhook" + "go.step.sm/linkedca" +) + +var ErrWebhookDenied = errors.New("webhook server did not allow request") + +type WebhookSetter interface { + SetWebhook(string, any) +} + +type WebhookController struct { + client *http.Client + webhooks []*Webhook + certType linkedca.Webhook_CertType + TemplateData WebhookSetter +} + +// Enrich fetches data from remote servers and adds returned data to the +// templateData +func (wc *WebhookController) Enrich(req *webhook.RequestBody) error { + if wc == nil { + return nil + } + for _, wh := range wc.webhooks { + if wh.Kind != linkedca.Webhook_ENRICHING.String() { + continue + } + if !wc.isCertTypeOK(wh) { + continue + } + resp, err := wh.Do(wc.client, req, wc.TemplateData) + if err != nil { + return err + } + if !resp.Allow { + return ErrWebhookDenied + } + wc.TemplateData.SetWebhook(wh.Name, resp.Data) + } + return nil +} + +// Authorize checks that all remote servers allow the request +func (wc *WebhookController) Authorize(req *webhook.RequestBody) error { + if wc == nil { + return nil + } + for _, wh := range wc.webhooks { + if wh.Kind != linkedca.Webhook_AUTHORIZING.String() { + continue + } + if !wc.isCertTypeOK(wh) { + continue + } + resp, err := wh.Do(wc.client, req, wc.TemplateData) + if err != nil { + return err + } + if !resp.Allow { + return ErrWebhookDenied + } + } + return nil +} + +func (wc *WebhookController) isCertTypeOK(wh *Webhook) bool { + if wc.certType == linkedca.Webhook_ALL { + return true + } + if wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == "" { + return true + } + return wc.certType.String() == wh.CertType +} + +type Webhook struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Kind string `json:"kind"` + DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"` + CertType string `json:"certType"` + Secret string `json:"-"` + BearerToken string `json:"-"` + BasicAuth struct { + Username string + Password string + } `json:"-"` +} + +func (w *Webhook) Do(client *http.Client, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) { + tmpl, err := template.New("url").Funcs(templates.StepFuncMap()).Parse(w.URL) + if err != nil { + return nil, err + } + buf := &bytes.Buffer{} + if err := tmpl.Execute(buf, data); err != nil { + return nil, err + } + url := buf.String() + + /* + Sending the token to the webhook server is a security risk. A K8sSA + token can be reused multiple times. The webhook can misuse it to get + fake certificates. A webhook can misuse any other token to get its own + certificate before responding. + switch tmpl := data.(type) { + case x509util.TemplateData: + reqBody.Token = tmpl[x509util.TokenKey] + case sshutil.TemplateData: + reqBody.Token = tmpl[sshutil.TokenKey] + } + */ + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + reqBody.Timestamp = time.Now() + + reqBytes, err := json.Marshal(reqBody) + if err != nil { + return nil, err + } + + retries := 1 +retry: + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(reqBytes)) + if err != nil { + return nil, err + } + + secret, err := base64.StdEncoding.DecodeString(w.Secret) + if err != nil { + return nil, err + } + sig := hmac.New(sha256.New, secret).Sum(reqBytes) + req.Header.Set("X-Smallstep-Signature", hex.EncodeToString(sig)) + req.Header.Set("X-Smallstep-Webhook-ID", w.ID) + + if w.BearerToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.BearerToken)) + } else if w.BasicAuth.Username != "" || w.BasicAuth.Password != "" { + req.SetBasicAuth(w.BasicAuth.Username, w.BasicAuth.Password) + } + + if w.DisableTLSClientAuth { + transport, ok := client.Transport.(*http.Transport) + if !ok { + return nil, errors.New("client transport is not a *http.Transport") + } + transport = transport.Clone() + tlsConfig := transport.TLSClientConfig.Clone() + tlsConfig.GetClientCertificate = nil + tlsConfig.Certificates = nil + transport.TLSClientConfig = tlsConfig + client = &http.Client{ + Transport: transport, + } + } + resp, err := client.Do(req) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return nil, err + } else if retries > 0 { + retries-- + time.Sleep(time.Second) + goto retry + } + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Printf("Failed to close body of response from %s", w.URL) + } + }() + if resp.StatusCode >= 500 && retries > 0 { + retries-- + time.Sleep(time.Second) + goto retry + } + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("Webhook server responded with %d", resp.StatusCode) + } + + respBody := &webhook.ResponseBody{} + if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil { + return nil, err + } + + return respBody, nil +} diff --git a/authority/provisioner/webhook_test.go b/authority/provisioner/webhook_test.go new file mode 100644 index 00000000..a7895638 --- /dev/null +++ b/authority/provisioner/webhook_test.go @@ -0,0 +1,473 @@ +package provisioner + +import ( + "crypto/hmac" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/certificates/webhook" + "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" +) + +func TestWebhookController_isCertTypeOK(t *testing.T) { + type test struct { + wc *WebhookController + wh *Webhook + want bool + } + tests := map[string]test{ + "all/all": { + wc: &WebhookController{certType: linkedca.Webhook_ALL}, + wh: &Webhook{CertType: linkedca.Webhook_ALL.String()}, + want: true, + }, + "all/x509": { + wc: &WebhookController{certType: linkedca.Webhook_ALL}, + wh: &Webhook{CertType: linkedca.Webhook_X509.String()}, + want: true, + }, + "all/ssh": { + wc: &WebhookController{certType: linkedca.Webhook_ALL}, + wh: &Webhook{CertType: linkedca.Webhook_SSH.String()}, + want: true, + }, + `all/""`: { + wc: &WebhookController{certType: linkedca.Webhook_ALL}, + wh: &Webhook{}, + want: true, + }, + "x509/all": { + wc: &WebhookController{certType: linkedca.Webhook_X509}, + wh: &Webhook{CertType: linkedca.Webhook_ALL.String()}, + want: true, + }, + "x509/x509": { + wc: &WebhookController{certType: linkedca.Webhook_X509}, + wh: &Webhook{CertType: linkedca.Webhook_X509.String()}, + want: true, + }, + "x509/ssh": { + wc: &WebhookController{certType: linkedca.Webhook_X509}, + wh: &Webhook{CertType: linkedca.Webhook_SSH.String()}, + want: false, + }, + `x509/""`: { + wc: &WebhookController{certType: linkedca.Webhook_X509}, + wh: &Webhook{}, + want: true, + }, + "ssh/all": { + wc: &WebhookController{certType: linkedca.Webhook_SSH}, + wh: &Webhook{CertType: linkedca.Webhook_ALL.String()}, + want: true, + }, + "ssh/x509": { + wc: &WebhookController{certType: linkedca.Webhook_SSH}, + wh: &Webhook{CertType: linkedca.Webhook_X509.String()}, + want: false, + }, + "ssh/ssh": { + wc: &WebhookController{certType: linkedca.Webhook_SSH}, + wh: &Webhook{CertType: linkedca.Webhook_SSH.String()}, + want: true, + }, + `ssh/""`: { + wc: &WebhookController{certType: linkedca.Webhook_SSH}, + wh: &Webhook{}, + want: true, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equals(t, test.want, test.wc.isCertTypeOK(test.wh)) + }) + } +} + +func TestWebhookController_Enrich(t *testing.T) { + type test struct { + ctl *WebhookController + req *webhook.RequestBody + responses []*webhook.ResponseBody + expectErr bool + expectTemplateData any + } + tests := map[string]test{ + "ok/no enriching webhooks": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}}, + TemplateData: nil, + }, + req: &webhook.RequestBody{}, + responses: nil, + expectErr: false, + expectTemplateData: nil, + }, + "ok/one webhook": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}}, + TemplateData: x509util.TemplateData{}, + }, + 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"}}}, + }, + "ok/two webhooks": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{ + {Name: "people", Kind: "ENRICHING"}, + {Name: "devices", Kind: "ENRICHING"}, + }, + TemplateData: x509util.TemplateData{}, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{ + {Allow: true, Data: map[string]any{"role": "bar"}}, + {Allow: true, Data: map[string]any{"serial": "123"}}, + }, + expectErr: false, + expectTemplateData: x509util.TemplateData{ + "Webhooks": map[string]any{ + "devices": map[string]any{"serial": "123"}, + "people": map[string]any{"role": "bar"}, + }, + }, + }, + "ok/x509 only": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{ + {Name: "people", Kind: "ENRICHING", CertType: linkedca.Webhook_SSH.String()}, + {Name: "devices", Kind: "ENRICHING"}, + }, + TemplateData: x509util.TemplateData{}, + certType: linkedca.Webhook_X509, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{ + {Allow: true, Data: map[string]any{"role": "bar"}}, + {Allow: true, Data: map[string]any{"serial": "123"}}, + }, + expectErr: false, + expectTemplateData: x509util.TemplateData{ + "Webhooks": map[string]any{ + "devices": map[string]any{"serial": "123"}, + }, + }, + }, + "deny": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}}, + TemplateData: x509util.TemplateData{}, + }, + 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) { + for i, wh := range test.ctl.webhooks { + var j = i + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewEncoder(w).Encode(test.responses[j]) + assert.FatalError(t, err) + })) + // nolint: gocritic // defer in loop isn't a memory leak + defer ts.Close() + wh.URL = ts.URL + } + + err := test.ctl.Enrich(test.req) + if (err != nil) != test.expectErr { + t.Fatalf("Got err %v, want %v", err, test.expectErr) + } + assert.Equals(t, test.expectTemplateData, test.ctl.TemplateData) + }) + } +} + +func TestWebhookController_Authorize(t *testing.T) { + type test struct { + ctl *WebhookController + req *webhook.RequestBody + responses []*webhook.ResponseBody + expectErr bool + } + tests := map[string]test{ + "ok/no enriching webhooks": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}}, + }, + req: &webhook.RequestBody{}, + responses: nil, + expectErr: false, + }, + "ok": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}}, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{{Allow: true}}, + expectErr: false, + }, + "ok/ssh only": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING", CertType: linkedca.Webhook_X509.String()}}, + certType: linkedca.Webhook_SSH, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{{Allow: false}}, + expectErr: false, + }, + "deny": { + ctl: &WebhookController{ + client: http.DefaultClient, + webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}}, + }, + req: &webhook.RequestBody{}, + responses: []*webhook.ResponseBody{{Allow: false}}, + expectErr: true, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + for i, wh := range test.ctl.webhooks { + var j = i + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewEncoder(w).Encode(test.responses[j]) + assert.FatalError(t, err) + })) + // nolint: gocritic // defer in loop isn't a memory leak + defer ts.Close() + wh.URL = ts.URL + } + + err := test.ctl.Authorize(test.req) + if (err != nil) != test.expectErr { + t.Fatalf("Got err %v, want %v", err, test.expectErr) + } + }) + } +} + +func TestWebhook_Do(t *testing.T) { + csr := parseCertificateRequest(t, "testdata/certs/ecdsa.csr") + type test struct { + webhook Webhook + dataArg any + webhookResponse webhook.ResponseBody + expectPath string + errStatusCode int + serverErrMsg string + expectErr error + // expectToken any + } + tests := map[string]test{ + "ok": { + webhook: Webhook{ + ID: "abc123", + Secret: "c2VjcmV0Cg==", + }, + webhookResponse: webhook.ResponseBody{ + Data: map[string]interface{}{"role": "dba"}, + }, + }, + "ok/bearer": { + webhook: Webhook{ + ID: "abc123", + Secret: "c2VjcmV0Cg==", + BearerToken: "mytoken", + }, + webhookResponse: webhook.ResponseBody{ + Data: map[string]interface{}{"role": "dba"}, + }, + }, + "ok/basic": { + webhook: Webhook{ + ID: "abc123", + Secret: "c2VjcmV0Cg==", + BasicAuth: struct { + Username string + Password string + }{ + Username: "myuser", + Password: "mypass", + }, + }, + webhookResponse: webhook.ResponseBody{ + Data: map[string]interface{}{"role": "dba"}, + }, + }, + "ok/templated-url": { + webhook: Webhook{ + ID: "abc123", + // scheme, host, port will come from test server + URL: "/users/{{ .username }}?region={{ .region }}", + Secret: "c2VjcmV0Cg==", + }, + dataArg: map[string]interface{}{"username": "areed", "region": "central"}, + webhookResponse: webhook.ResponseBody{ + Data: map[string]interface{}{"role": "dba"}, + }, + expectPath: "/users/areed?region=central", + }, + /* + "ok/token from ssh template": { + webhook: Webhook{ + ID: "abc123", + Secret: "c2VjcmV0Cg==", + }, + webhookResponse: webhook.ResponseBody{ + Data: map[string]interface{}{"role": "dba"}, + }, + dataArg: sshutil.TemplateData{sshutil.TokenKey: "token"}, + expectToken: "token", + }, + "ok/token from x509 template": { + webhook: Webhook{ + ID: "abc123", + Secret: "c2VjcmV0Cg==", + }, + webhookResponse: webhook.ResponseBody{ + Data: map[string]interface{}{"role": "dba"}, + }, + dataArg: x509util.TemplateData{sshutil.TokenKey: "token"}, + expectToken: "token", + }, + */ + "ok/allow": { + webhook: Webhook{ + ID: "abc123", + Secret: "c2VjcmV0Cg==", + }, + webhookResponse: webhook.ResponseBody{ + Allow: true, + }, + }, + "fail/404": { + webhook: Webhook{ + ID: "abc123", + Secret: "c2VjcmV0Cg==", + }, + webhookResponse: webhook.ResponseBody{ + Data: map[string]interface{}{"role": "dba"}, + }, + errStatusCode: 404, + serverErrMsg: "item not found", + expectErr: errors.New("Webhook server responded with 404"), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := r.Header.Get("X-Smallstep-Webhook-ID") + assert.Equals(t, tc.webhook.ID, id) + + sig, err := hex.DecodeString(r.Header.Get("X-Smallstep-Signature")) + assert.FatalError(t, err) + + body, err := io.ReadAll(r.Body) + assert.FatalError(t, err) + + secret, err := base64.StdEncoding.DecodeString(tc.webhook.Secret) + assert.FatalError(t, err) + mac := hmac.New(sha256.New, secret).Sum(body) + assert.True(t, hmac.Equal(sig, mac)) + + switch { + case tc.webhook.BearerToken != "": + ah := fmt.Sprintf("Bearer %s", tc.webhook.BearerToken) + assert.Equals(t, ah, r.Header.Get("Authorization")) + case tc.webhook.BasicAuth.Username != "" || tc.webhook.BasicAuth.Password != "": + whReq, err := http.NewRequest("", "", http.NoBody) + assert.FatalError(t, err) + whReq.SetBasicAuth(tc.webhook.BasicAuth.Username, tc.webhook.BasicAuth.Password) + ah := whReq.Header.Get("Authorization") + assert.Equals(t, ah, whReq.Header.Get("Authorization")) + default: + assert.Equals(t, "", r.Header.Get("Authorization")) + } + + if tc.expectPath != "" { + assert.Equals(t, tc.expectPath, r.URL.Path+"?"+r.URL.RawQuery) + } + + if tc.errStatusCode != 0 { + http.Error(w, tc.serverErrMsg, tc.errStatusCode) + return + } + + reqBody := new(webhook.RequestBody) + err = json.Unmarshal(body, reqBody) + assert.FatalError(t, err) + // assert.Equals(t, tc.expectToken, reqBody.Token) + + err = json.NewEncoder(w).Encode(tc.webhookResponse) + assert.FatalError(t, err) + })) + defer ts.Close() + + tc.webhook.URL = ts.URL + tc.webhook.URL + + reqBody, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr)) + assert.FatalError(t, err) + got, err := tc.webhook.Do(http.DefaultClient, reqBody, tc.dataArg) + if tc.expectErr != nil { + assert.Equals(t, tc.expectErr.Error(), err.Error()) + return + } + assert.FatalError(t, err) + + assert.Equals(t, got, &tc.webhookResponse) + }) + } + + t.Run("disableTLSClientAuth", func(t *testing.T) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{}")) + })) + ts.TLS.ClientAuth = tls.RequireAnyClientCert + wh := Webhook{ + URL: ts.URL, + } + cert, err := tls.LoadX509KeyPair("testdata/certs/foo.crt", "testdata/secrets/foo.key") + assert.FatalError(t, err) + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{cert}, + } + client := &http.Client{ + Transport: transport, + } + reqBody, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr)) + assert.FatalError(t, err) + _, err = wh.Do(client, reqBody, nil) + assert.FatalError(t, err) + + wh.DisableTLSClientAuth = true + _, err = wh.Do(client, reqBody, nil) + assert.Error(t, err) + }) +} diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 9f9a0e4e..e60533b7 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -12,6 +12,7 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/sshutil" "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" "github.com/smallstep/certificates/errs" ) @@ -245,6 +246,7 @@ func (p *X5C) AuthorizeSign(ctx 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), }, nil } @@ -332,5 +334,7 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, &sshCertDefaultValidator{}, // 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), ), nil } diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go index eb4f7def..437b7661 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -389,9 +389,10 @@ lgsqsR63is+0YQ== tc := tt(t) if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -460,15 +461,16 @@ func TestX5C_AuthorizeSign(t *testing.T) { tc := tt(t) if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCoder interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { if assert.NotNil(t, opts) { - assert.Equals(t, 9, len(opts)) + assert.Equals(t, 10, len(opts)) for _, o := range opts { switch v := o.(type) { case *X5C: @@ -493,6 +495,8 @@ func TestX5C_AuthorizeSign(t *testing.T) { assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration()) case *x509NamePolicyValidator: assert.Equals(t, nil, v.policyEngine) + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } @@ -545,9 +549,10 @@ func TestX5C_AuthorizeRevoke(t *testing.T) { tc := tt(t) if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -595,9 +600,10 @@ func TestX5C_AuthorizeRenew(t *testing.T) { NotAfter: now.Add(time.Hour), }); err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -756,9 +762,10 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { tc := tt(t) if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCoder interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -794,15 +801,17 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) { assert.Equals(t, nil, v.userPolicyEngine) assert.Equals(t, nil, v.hostPolicyEngine) case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc: + case *WebhookController: + assert.Len(t, 0, v.webhooks) default: assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v)) } tot++ } if len(tc.claims.Step.SSH.CertType) > 0 { - assert.Equals(t, tot, 11) + assert.Equals(t, tot, 12) } else { - assert.Equals(t, tot, 9) + assert.Equals(t, tot, 10) } } } diff --git a/authority/provisioners.go b/authority/provisioners.go index b98b1811..bfa4eae5 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -25,6 +25,44 @@ import ( "github.com/smallstep/certificates/errs" ) +type raProvisioner interface { + RAInfo() *provisioner.RAInfo +} + +type attProvisioner interface { + AttestationData() *provisioner.AttestationData +} + +// wrapProvisioner wraps the given provisioner with RA information and +// attestation data. +func wrapProvisioner(p provisioner.Interface, attData *provisioner.AttestationData) *wrappedProvisioner { + var raInfo *provisioner.RAInfo + if rap, ok := p.(raProvisioner); ok { + raInfo = rap.RAInfo() + } + + return &wrappedProvisioner{ + Interface: p, + attestationData: attData, + raInfo: raInfo, + } +} + +// wrappedProvisioner implements raProvisioner and attProvisioner. +type wrappedProvisioner struct { + provisioner.Interface + attestationData *provisioner.AttestationData + raInfo *provisioner.RAInfo +} + +func (p *wrappedProvisioner) AttestationData() *provisioner.AttestationData { + return p.attestationData +} + +func (p *wrappedProvisioner) RAInfo() *provisioner.RAInfo { + return p.raInfo +} + // GetEncryptedKey returns the JWE key corresponding to the given kid argument. func (a *Authority) GetEncryptedKey(kid string) (string, error) { a.adminMutex.RLock() @@ -144,6 +182,7 @@ func (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner. GetIdentityFunc: a.getIdentityFunc, AuthorizeRenewFunc: a.authorizeRenewFunc, AuthorizeSSHRenewFunc: a.authorizeSSHRenewFunc, + WebhookClient: a.webhookClient, }, nil } @@ -493,9 +532,63 @@ func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options { } } } + for _, wh := range p.Webhooks { + whCert := webhookToCertificates(wh) + ops.Webhooks = append(ops.Webhooks, whCert) + } return ops } +func webhookToCertificates(wh *linkedca.Webhook) *provisioner.Webhook { + pwh := &provisioner.Webhook{ + ID: wh.Id, + Name: wh.Name, + URL: wh.Url, + Kind: wh.Kind.String(), + Secret: wh.Secret, + DisableTLSClientAuth: wh.DisableTlsClientAuth, + CertType: wh.CertType.String(), + } + + switch a := wh.GetAuth().(type) { + case *linkedca.Webhook_BearerToken: + pwh.BearerToken = a.BearerToken.BearerToken + case *linkedca.Webhook_BasicAuth: + pwh.BasicAuth.Username = a.BasicAuth.Username + pwh.BasicAuth.Password = a.BasicAuth.Password + } + + return pwh +} + +func provisionerWebhookToLinkedca(pwh *provisioner.Webhook) *linkedca.Webhook { + lwh := &linkedca.Webhook{ + Id: pwh.ID, + Name: pwh.Name, + Url: pwh.URL, + Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[pwh.Kind]), + Secret: pwh.Secret, + DisableTlsClientAuth: pwh.DisableTLSClientAuth, + CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[pwh.CertType]), + } + if pwh.BearerToken != "" { + lwh.Auth = &linkedca.Webhook_BearerToken{ + BearerToken: &linkedca.BearerToken{ + BearerToken: pwh.BearerToken, + }, + } + } else if pwh.BasicAuth.Username != "" || pwh.BasicAuth.Password != "" { + lwh.Auth = &linkedca.Webhook_BasicAuth{ + BasicAuth: &linkedca.BasicAuth{ + Username: pwh.BasicAuth.Username, + Password: pwh.BasicAuth.Password, + }, + } + } + + return lwh +} + func durationsToCertificates(d *linkedca.Durations) (min, max, def *provisioner.Duration, err error) { if len(d.Min) > 0 { min, err = provisioner.NewDuration(d.Min) @@ -621,12 +714,12 @@ func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims { return lc } -func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, error) { +func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, []*linkedca.Webhook, error) { var err error var x509Template, sshTemplate *linkedca.Template if p == nil { - return nil, nil, nil + return nil, nil, nil, nil } if p.X509 != nil && p.X509.HasTemplate() { @@ -640,7 +733,7 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, * } else if p.X509.TemplateFile != "" { filename := step.Abs(p.X509.TemplateFile) if x509Template.Template, err = os.ReadFile(filename); err != nil { - return nil, nil, errors.Wrap(err, "error reading x509 template") + return nil, nil, nil, errors.Wrap(err, "error reading x509 template") } } } @@ -656,12 +749,17 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, * } else if p.SSH.TemplateFile != "" { filename := step.Abs(p.SSH.TemplateFile) if sshTemplate.Template, err = os.ReadFile(filename); err != nil { - return nil, nil, errors.Wrap(err, "error reading ssh template") + return nil, nil, nil, errors.Wrap(err, "error reading ssh template") } } } - return x509Template, sshTemplate, nil + var webhooks []*linkedca.Webhook + for _, pwh := range p.Webhooks { + webhooks = append(webhooks, provisionerWebhookToLinkedca(pwh)) + } + + return x509Template, sshTemplate, webhooks, nil } func provisionerPEMToLinkedca(b []byte) [][]byte { @@ -879,7 +977,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, error) { switch p := p.(type) { case *provisioner.JWK: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -902,9 +1000,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.OIDC: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -929,9 +1028,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.GCP: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -953,9 +1053,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.AWS: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -976,9 +1077,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.Azure: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -1002,9 +1104,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.ACME: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -1025,9 +1128,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.X5C: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -1045,9 +1149,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.K8sSA: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -1065,6 +1170,7 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.SSHPOP: return &linkedca.Provisioner{ @@ -1079,7 +1185,7 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), }, nil case *provisioner.SCEP: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -1102,9 +1208,10 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil case *provisioner.Nebula: - x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options) if err != nil { return nil, err } @@ -1122,6 +1229,7 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Claims: claimsToLinkedca(p.Claims), X509Template: x509Template, SshTemplate: sshTemplate, + Webhooks: webhooks, }, nil default: return nil, fmt.Errorf("provisioner %s not implemented", p.GetType()) diff --git a/authority/provisioners_test.go b/authority/provisioners_test.go index 56cd16b1..6ef62223 100644 --- a/authority/provisioners_test.go +++ b/authority/provisioners_test.go @@ -16,6 +16,7 @@ import ( "github.com/smallstep/certificates/db" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" + "go.step.sm/linkedca" ) func TestGetEncryptedKey(t *testing.T) { @@ -57,9 +58,10 @@ func TestGetEncryptedKey(t *testing.T) { ek, err := tc.a.GetEncryptedKey(tc.kid) if err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -107,9 +109,10 @@ func TestGetProvisioners(t *testing.T) { ps, next, err := tc.a.GetProvisioners("", 0) if err != nil { if assert.NotNil(t, tc.err) { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -251,3 +254,82 @@ func TestAuthority_LoadProvisionerByCertificate(t *testing.T) { }) } } + +func TestProvisionerWebhookToLinkedca(t *testing.T) { + type test struct { + lwh *linkedca.Webhook + pwh *provisioner.Webhook + } + tests := map[string]test{ + "empty": test{ + lwh: &linkedca.Webhook{}, + pwh: &provisioner.Webhook{Kind: "NO_KIND", CertType: "ALL"}, + }, + "enriching ssh basic auth": test{ + lwh: &linkedca.Webhook{ + Id: "abc123", + Name: "people", + Url: "https://localhost", + Kind: linkedca.Webhook_ENRICHING, + Secret: "secret", + Auth: &linkedca.Webhook_BasicAuth{ + BasicAuth: &linkedca.BasicAuth{ + Username: "user", + Password: "pass", + }, + }, + DisableTlsClientAuth: true, + CertType: linkedca.Webhook_SSH, + }, + pwh: &provisioner.Webhook{ + ID: "abc123", + Name: "people", + URL: "https://localhost", + Kind: "ENRICHING", + Secret: "secret", + BasicAuth: struct { + Username string + Password string + }{ + Username: "user", + Password: "pass", + }, + DisableTLSClientAuth: true, + CertType: "SSH", + }, + }, + "authorizing x509 bearer auth": test{ + lwh: &linkedca.Webhook{ + Id: "abc123", + Name: "people", + Url: "https://localhost", + Kind: linkedca.Webhook_AUTHORIZING, + Secret: "secret", + Auth: &linkedca.Webhook_BearerToken{ + BearerToken: &linkedca.BearerToken{ + BearerToken: "tkn", + }, + }, + CertType: linkedca.Webhook_X509, + }, + pwh: &provisioner.Webhook{ + ID: "abc123", + Name: "people", + URL: "https://localhost", + Kind: "AUTHORIZING", + Secret: "secret", + BearerToken: "tkn", + CertType: "X509", + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + gotLWH := provisionerWebhookToLinkedca(test.pwh) + assert.Equals(t, test.lwh, gotLWH) + + gotPWH := webhookToCertificates(test.lwh) + assert.Equals(t, test.pwh, gotPWH) + }) + } +} diff --git a/authority/ssh.go b/authority/ssh.go index 1b243b39..7d990904 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -20,6 +20,7 @@ import ( "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/templates" + "github.com/smallstep/certificates/webhook" ) const ( @@ -161,6 +162,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi opts.Backdate = a.config.AuthorityConfig.Backdate.Duration var prov provisioner.Interface + var webhookCtl webhookController for _, op := range signOpts { switch o := op.(type) { // Capture current provisioner @@ -185,6 +187,10 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi return nil, errs.BadRequestErr(err, "error validating ssh certificate options") } + // call webhooks + case webhookController: + webhookCtl = o + default: return nil, errs.InternalServer("authority.SignSSH: invalid extra option type %T", o) } @@ -198,6 +204,14 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi Key: key, } + // Call enriching webhooks + if err := callEnrichingWebhooksSSH(webhookCtl, cr); err != nil { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(err, err.Error()), + errs.WithKeyVal("signOptions", signOpts), + ) + } + // Create certificate from template. certificate, err := sshutil.NewCertificate(cr, certOptions...) if err != nil { @@ -262,6 +276,13 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi ) } + // Send certificate to webhooks for authorization + if err := callAuthorizingWebhooksSSH(webhookCtl, certificate, certTpl); err != nil { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(err, "authority.SignSSH: error signing certificate"), + ) + } + // Sign certificate. cert, err := sshutil.CreateCertificate(certTpl, signer) if err != nil { @@ -631,3 +652,37 @@ func (a *Authority) getAddUserCommand(principal string) string { } return strings.ReplaceAll(cmd, "", principal) } + +func callEnrichingWebhooksSSH(webhookCtl webhookController, cr sshutil.CertificateRequest) error { + if webhookCtl == nil { + return nil + } + whEnrichReq, err := webhook.NewRequestBody( + webhook.WithSSHCertificateRequest(cr), + ) + if err != nil { + return err + } + if err := webhookCtl.Enrich(whEnrichReq); err != nil { + return err + } + + return nil +} + +func callAuthorizingWebhooksSSH(webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) error { + if webhookCtl == nil { + return nil + } + whAuthBody, err := webhook.NewRequestBody( + webhook.WithSSHCertificate(cert, certTpl), + ) + if err != nil { + return err + } + if err := webhookCtl.Authorize(whAuthBody); err != nil { + return err + } + + return nil +} diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 73916c03..b24be941 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -178,6 +178,17 @@ func TestAuthority_SignSSH(t *testing.T) { }`}, }, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"})) assert.FatalError(t, err) + enrichTemplateData := sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"}) + enrichTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{ + SSH: &provisioner.SSHOptions{Template: `{ + "type": "{{ .Type }}", + "keyId": "{{ .KeyID }}", + "principals": {{ toJson .Webhooks.people.role }}, + "extensions": {{ set .Extensions "login@github.com" .Insecure.User.username | toJson }}, + "criticalOptions": {{ toJson .CriticalOptions }} + }`}, + }, enrichTemplateData) + assert.FatalError(t, err) userFailTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{ SSH: &provisioner.SSHOptions{Template: `{{ fail "an error"}}`}, }, sshutil.CreateTemplateData(sshutil.UserCert, "key-id", []string{"user"})) @@ -255,6 +266,7 @@ func TestAuthority_SignSSH(t *testing.T) { {"ok-opts-validator", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false}, {"ok-opts-modifier", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false}, {"ok-custom-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{"user", "admin"}}, false}, + {"ok-enrich-template", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{enrichTemplate, userOptions, &mockWebhookController{templateData: enrichTemplateData, respData: map[string]any{"people": map[string]any{"role": []string{"user", "eng"}}}}}}, want{CertType: ssh.UserCert, Principals: []string{"user", "eng"}}, false}, {"ok-user-policy", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, {"ok-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false}, {"fail-opts-type", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{userTemplate}}, want{}, true}, @@ -275,6 +287,8 @@ func TestAuthority_SignSSH(t *testing.T) { {"fail-host-policy", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true}, {"fail-host-policy-with-user-cert", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{}, true}, {"fail-host-policy-with-bad-host", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"example.com"}}, []provisioner.SignOption{badHostTemplate}}, want{}, true}, + {"fail-enriching-webhooks", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, &mockWebhookController{enrichErr: provisioner.ErrWebhookDenied}}}, want{}, true}, + {"fail-authorizing-webhooks", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, &mockWebhookController{authorizeErr: provisioner.ErrWebhookDenied}}}, want{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -760,8 +774,8 @@ func TestAuthority_GetSSHBastion(t *testing.T) { t.Errorf("Authority.GetSSHBastion() error = %v, wantErr %v", err, tt.wantErr) return } else if err != nil { - _, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") + var sc render.StatusCodedError + assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Authority.GetSSHBastion() = %v, want %v", got, tt.want) @@ -851,8 +865,9 @@ func TestAuthority_GetSSHHosts(t *testing.T) { if err != nil { if assert.NotNil(t, tc.err) { var sc render.StatusCodedError - assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { @@ -1078,8 +1093,9 @@ func TestAuthority_RekeySSH(t *testing.T) { if err != nil { if assert.NotNil(t, tc.err) { var sc render.StatusCodedError - assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tc.code) + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tc.code) + } assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { diff --git a/authority/tls.go b/authority/tls.go index 84106002..184944a0 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -31,6 +31,7 @@ import ( "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" "github.com/smallstep/nosql/database" + "github.com/smallstep/certificates/webhook" ) // GetTLSOptions returns the tls options configured. @@ -99,7 +100,8 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign var prov provisioner.Interface var pInfo *casapi.ProvisionerInfo - var attData provisioner.AttestationData + var attData *provisioner.AttestationData + var webhookCtl webhookController for _, op := range extraOpts { switch k := op.(type) { // Capture current provisioner @@ -137,14 +139,25 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Extra information from ACME attestations. case provisioner.AttestationData: - attData = k - // TODO(mariano,areed): remove me once attData is used. - _ = attData + attData = &k + + // Capture the provisioner's webhook controller + case webhookController: + webhookCtl = k + default: return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...) } } + if err := callEnrichingWebhooksX509(webhookCtl, attData, csr); err != nil { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(err, err.Error()), + errs.WithKeyVal("csr", csr), + errs.WithKeyVal("signOptions", signOpts), + ) + } + cert, err := x509util.NewCertificate(csr, certOptions...) if err != nil { var te *x509util.TemplateError @@ -229,6 +242,14 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign ) } + // Send certificate to webhooks for authorization + if err := callAuthorizingWebhooksX509(webhookCtl, cert, leaf, attData); err != nil { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(err, "error creating certificate"), + opts..., + ) + } + // Sign certificate lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ @@ -243,6 +264,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...) + + // Wrap provisioner with extra information. + prov = wrapProvisioner(prov, attData) + + // Store certificate in the db. if err = a.storeCertificate(prov, fullchain); err != nil { if !errors.Is(err, db.ErrNotImplemented) { return nil, errs.Wrap(http.StatusInternalServerError, err, @@ -300,7 +326,7 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // Create new certificate from previous values. // Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS. newCert := &x509.Certificate{ - Subject: oldCert.Subject, + RawSubject: oldCert.RawSubject, KeyUsage: oldCert.KeyUsage, UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions, ExtKeyUsage: oldCert.ExtKeyUsage, @@ -884,3 +910,51 @@ func templatingError(err error) error { } return errors.Wrap(cause, "error applying certificate template") } + +func callEnrichingWebhooksX509(webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) error { + if webhookCtl == nil { + return nil + } + var attested *webhook.AttestationData + if attData != nil { + attested = &webhook.AttestationData{ + PermanentIdentifier: attData.PermanentIdentifier, + } + } + whEnrichReq, err := webhook.NewRequestBody( + webhook.WithX509CertificateRequest(csr), + webhook.WithAttestationData(attested), + ) + if err != nil { + return err + } + if err := webhookCtl.Enrich(whEnrichReq); err != nil { + return err + } + + return nil +} + +func callAuthorizingWebhooksX509(webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) error { + if webhookCtl == nil { + return nil + } + var attested *webhook.AttestationData + if attData != nil { + attested = &webhook.AttestationData{ + PermanentIdentifier: attData.PermanentIdentifier, + } + } + whAuthBody, err := webhook.NewRequestBody( + webhook.WithX509Certificate(cert, leaf), + webhook.WithAttestationData(attested), + ) + if err != nil { + return err + } + if err := webhookCtl.Authorize(whAuthBody); err != nil { + return err + } + + return nil +} diff --git a/authority/tls_test.go b/authority/tls_test.go index 1d542cb9..37066843 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -18,8 +18,6 @@ import ( "testing" "time" - "gopkg.in/square/go-jose.v2/jwt" - "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/minica" @@ -61,6 +59,15 @@ func (m *certificateDurationEnforcer) Enforce(cert *x509.Certificate) error { return nil } +type certificateChainDB struct { + db.MockAuthDB + MStoreCertificateChain func(provisioner.Interface, ...*x509.Certificate) error +} + +func (d *certificateChainDB) StoreCertificateChain(p provisioner.Interface, certs ...*x509.Certificate) error { + return d.MStoreCertificateChain(p, certs...) +} + func getDefaultIssuer(a *Authority) *x509.Certificate { return a.x509CAService.(*softcas.SoftCAS).CertificateChain[len(a.x509CAService.(*softcas.SoftCAS).CertificateChain)-1] } @@ -134,6 +141,13 @@ func generateIntermidiateCertificate(t *testing.T, issuer *x509.Certificate, sig return cert, priv } +func withSubject(sub pkix.Name) provisioner.CertificateModifierFunc { + return func(crt *x509.Certificate, _ provisioner.SignOptions) error { + crt.Subject = sub + return nil + } +} + func withProvisionerOID(name, kid string) provisioner.CertificateModifierFunc { return func(crt *x509.Certificate, _ provisioner.SignOptions) error { b, err := asn1.Marshal(stepProvisionerASN1{ @@ -549,6 +563,36 @@ ZYtQ9Ot36qc= code: http.StatusForbidden, } }, + "fail enriching webhooks": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + csr.Raw = []byte("foo") + return &signTest{ + auth: a, + csr: csr, + extensionsCount: 7, + extraOpts: append(extraOpts, &mockWebhookController{ + enrichErr: provisioner.ErrWebhookDenied, + }), + signOpts: signOpts, + err: provisioner.ErrWebhookDenied, + code: http.StatusForbidden, + } + }, + "fail authorizing webhooks": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + csr.Raw = []byte("foo") + return &signTest{ + auth: a, + csr: csr, + extensionsCount: 7, + extraOpts: append(extraOpts, &mockWebhookController{ + authorizeErr: provisioner.ErrWebhookDenied, + }), + signOpts: signOpts, + err: provisioner.ErrWebhookDenied, + code: http.StatusForbidden, + } + }, "ok": func(t *testing.T) *signTest { csr := getCSR(t, priv) _a := testAuthority(t) @@ -636,6 +680,48 @@ ZYtQ9Ot36qc= extensionsCount: 6, } }, + "ok with enriching webhook": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + testAuthority := testAuthority(t) + testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template + p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc") + if !ok { + t.Fatal("provisioner not found") + } + p.(*provisioner.JWK).Options = &provisioner.Options{ + X509: &provisioner.X509Options{Template: `{ + "subject": {"commonName": {{ toJson .Webhooks.people.role }} }, + "dnsNames": {{ toJson .Insecure.CR.DNSNames }}, + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["serverAuth","clientAuth"] + }`}, + } + testExtraOpts, err := testAuthority.Authorize(ctx, token) + assert.FatalError(t, err) + testAuthority.db = &db.MockAuthDB{ + MStoreCertificate: func(crt *x509.Certificate) error { + assert.Equals(t, crt.Subject.CommonName, "smallstep test") + return nil + }, + } + for i, o := range testExtraOpts { + if wc, ok := o.(*provisioner.WebhookController); ok { + testExtraOpts[i] = &mockWebhookController{ + templateData: wc.TemplateData, + respData: map[string]any{"people": map[string]any{"role": "smallstep test"}}, + } + } + } + return &signTest{ + auth: testAuthority, + csr: csr, + extraOpts: testExtraOpts, + signOpts: signOpts, + notBefore: signOpts.NotBefore.Time().Truncate(time.Second), + notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + extensionsCount: 6, + } + }, "ok/csr with no template critical SAN extension": func(t *testing.T) *signTest { csr := getCSR(t, priv, func(csr *x509.CertificateRequest) { csr.Subject = pkix.Name{} @@ -697,7 +783,6 @@ ZYtQ9Ot36qc= aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template aa.db = &db.MockAuthDB{ MStoreCertificate: func(crt *x509.Certificate) error { - fmt.Println(crt.Subject) assert.Equals(t, crt.Subject.CommonName, "smallstep test") return nil }, @@ -723,6 +808,38 @@ ZYtQ9Ot36qc= extensionsCount: 6, } }, + "ok with attestation data": func(t *testing.T) *signTest { + csr := getCSR(t, priv) + aa := testAuthority(t) + aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template + aa.db = &certificateChainDB{ + MStoreCertificateChain: func(prov provisioner.Interface, certs ...*x509.Certificate) error { + p, ok := prov.(attProvisioner) + if assert.True(t, ok) { + assert.Equals(t, &provisioner.AttestationData{ + PermanentIdentifier: "1234567890", + }, p.AttestationData()) + } + if assert.Len(t, 2, certs) { + assert.Equals(t, certs[0].Subject.CommonName, "smallstep test") + assert.Equals(t, certs[1].Subject.CommonName, "smallstep Intermediate CA") + } + return nil + }, + } + + return &signTest{ + auth: aa, + csr: csr, + extraOpts: append(extraOpts, provisioner.AttestationData{ + PermanentIdentifier: "1234567890", + }), + signOpts: signOpts, + notBefore: signOpts.NotBefore.Time().Truncate(time.Second), + notAfter: signOpts.NotAfter.Time().Truncate(time.Second), + extensionsCount: 6, + } + }, } for name, genTestCase := range tests { @@ -844,6 +961,18 @@ func TestAuthority_Renew(t *testing.T) { withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), withSigner(issuer, signer)) + certExtraNames := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, + withSubject(pkix.Name{ + CommonName: "renew", + ExtraNames: []pkix.AttributeTypeAndValue{ + {Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}, Value: "dc"}, + }, + }), + withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), + withDefaultASN1DN(a.config.AuthorityConfig.Template), + withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), + withSigner(issuer, signer)) + certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), @@ -893,6 +1022,12 @@ func TestAuthority_Renew(t *testing.T) { cert: cert, }, nil }, + "ok/WithExtraNames": func() (*renewTest, error) { + return &renewTest{ + auth: a, + cert: certExtraNames, + }, nil + }, "ok/success-new-intermediate": func() (*renewTest, error) { rootCert, rootSigner := generateRootCertificate(t) intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner) @@ -955,15 +1090,14 @@ func TestAuthority_Renew(t *testing.T) { assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour))) tmplt := a.config.AuthorityConfig.Template - assert.Equals(t, leaf.Subject.String(), - pkix.Name{ - Country: []string{tmplt.Country}, - Organization: []string{tmplt.Organization}, - Locality: []string{tmplt.Locality}, - StreetAddress: []string{tmplt.StreetAddress}, - Province: []string{tmplt.Province}, - CommonName: tmplt.CommonName, - }.String()) + assert.Equals(t, leaf.RawSubject, tc.cert.RawSubject) + assert.Equals(t, leaf.Subject.Country, []string{tmplt.Country}) + assert.Equals(t, leaf.Subject.Organization, []string{tmplt.Organization}) + assert.Equals(t, leaf.Subject.Locality, []string{tmplt.Locality}) + assert.Equals(t, leaf.Subject.StreetAddress, []string{tmplt.StreetAddress}) + assert.Equals(t, leaf.Subject.Province, []string{tmplt.Province}) + assert.Equals(t, leaf.Subject.CommonName, tmplt.CommonName) + assert.Equals(t, leaf.Issuer, intermediate.Subject) assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256) @@ -1331,15 +1465,15 @@ func TestAuthority_Revoke(t *testing.T) { } }, "fail/nil-db": func() test { - cl := jwt.Claims{ + cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, - NotBefore: jwt.NewNumericDate(now), - Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return test{ @@ -1371,15 +1505,15 @@ func TestAuthority_Revoke(t *testing.T) { Err: errors.New("force"), })) - cl := jwt.Claims{ + cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, - NotBefore: jwt.NewNumericDate(now), - Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return test{ @@ -1411,15 +1545,15 @@ func TestAuthority_Revoke(t *testing.T) { Err: db.ErrAlreadyExists, })) - cl := jwt.Claims{ + cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, - NotBefore: jwt.NewNumericDate(now), - Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return test{ @@ -1450,15 +1584,15 @@ func TestAuthority_Revoke(t *testing.T) { }, })) - cl := jwt.Claims{ + cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, - NotBefore: jwt.NewNumericDate(now), - Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return test{ auth: _a, @@ -1542,15 +1676,15 @@ func TestAuthority_Revoke(t *testing.T) { }, })) - cl := jwt.Claims{ + cl := jose.Claims{ Subject: "sn", Issuer: validIssuer, - NotBefore: jwt.NewNumericDate(now), - Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: "44", } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) return test{ auth: a, diff --git a/authority/webhook.go b/authority/webhook.go new file mode 100644 index 00000000..d887e077 --- /dev/null +++ b/authority/webhook.go @@ -0,0 +1,8 @@ +package authority + +import "github.com/smallstep/certificates/webhook" + +type webhookController interface { + Enrich(*webhook.RequestBody) error + Authorize(*webhook.RequestBody) error +} diff --git a/authority/webhook_test.go b/authority/webhook_test.go new file mode 100644 index 00000000..b80c8f66 --- /dev/null +++ b/authority/webhook_test.go @@ -0,0 +1,27 @@ +package authority + +import ( + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/webhook" +) + +type mockWebhookController struct { + enrichErr error + authorizeErr error + templateData provisioner.WebhookSetter + respData map[string]any +} + +var _ webhookController = &mockWebhookController{} + +func (wc *mockWebhookController) Enrich(req *webhook.RequestBody) error { + for key, data := range wc.respData { + wc.templateData.SetWebhook(key, data) + } + + return wc.enrichErr +} + +func (wc *mockWebhookController) Authorize(req *webhook.RequestBody) error { + return wc.authorizeErr +} diff --git a/ca/adminClient.go b/ca/adminClient.go index 84a0d413..cde197af 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -1101,6 +1101,103 @@ retry: return nil } +func (c *AdminClient) CreateProvisionerWebhook(provisionerName string, wh *linkedca.Webhook) (*linkedca.Webhook, error) { + var retried bool + body, err := protojson.Marshal(wh) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "webhooks")}) + tok, err := c.generateAdminToken(u) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } +retry: + req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating POST %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client POST %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var webhook = new(linkedca.Webhook) + if err := readProtoJSON(resp.Body, webhook); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return webhook, nil +} + +func (c *AdminClient) UpdateProvisionerWebhook(provisionerName string, wh *linkedca.Webhook) (*linkedca.Webhook, error) { + var retried bool + body, err := protojson.Marshal(wh) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "webhooks", wh.Name)}) + tok, err := c.generateAdminToken(u) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } +retry: + req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client PUT %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var webhook = new(linkedca.Webhook) + if err := readProtoJSON(resp.Body, webhook); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return webhook, nil +} + +func (c *AdminClient) DeleteProvisionerWebhook(provisionerName, webhookName string) error { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", provisionerName, "webhooks", webhookName)}) + tok, err := c.generateAdminToken(u) + if err != nil { + return fmt.Errorf("error generating admin token: %w", err) + } +retry: + req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody) + if err != nil { + return fmt.Errorf("creating DELETE %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("client DELETE %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readAdminError(resp.Body) + } + return nil +} + func readAdminError(r io.ReadCloser) error { // TODO: not all errors can be read (i.e. 404); seems to be a bigger issue defer r.Close() diff --git a/ca/ca.go b/ca/ca.go index 01b321d7..880f7e46 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -156,17 +156,26 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { opts = append(opts, authority.WithDatabase(ca.opts.database)) } + if ca.opts.quiet { + opts = append(opts, authority.WithQuietInit()) + } + + webhookTransport := http.DefaultTransport.(*http.Transport).Clone() + opts = append(opts, authority.WithWebhookClient(&http.Client{Transport: webhookTransport})) + auth, err := authority.New(cfg, opts...) if err != nil { return nil, err } ca.auth = auth - tlsConfig, err := ca.getTLSConfig(auth) + tlsConfig, clientTLSConfig, err := ca.getTLSConfig(auth) if err != nil { return nil, err } + webhookTransport.TLSClientConfig = clientTLSConfig + // Using chi as the main router mux := chi.NewRouter() handler := http.Handler(mux) @@ -220,8 +229,14 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { if adminDB != nil { acmeAdminResponder := adminAPI.NewACMEAdminResponder() policyAdminResponder := adminAPI.NewPolicyAdminResponder() + webhookAdminResponder := adminAPI.NewWebhookAdminResponder() mux.Route("/admin", func(r chi.Router) { - adminAPI.Route(r, acmeAdminResponder, policyAdminResponder) + adminAPI.Route( + r, + adminAPI.WithACMEResponder(acmeAdminResponder), + adminAPI.WithPolicyResponder(policyAdminResponder), + adminAPI.WithWebhookResponder(webhookAdminResponder), + ) }) } } @@ -334,7 +349,7 @@ func (ca *CA) Run() error { if step.Contexts().GetCurrent() != nil { log.Printf("Current context: %s", step.Contexts().GetCurrent().Name) } - log.Printf("Config file: %s", ca.opts.configFile) + log.Printf("Config file: %s", ca.getConfigFileOutput()) baseURL := fmt.Sprintf("https://%s%s", authorityInfo.DNSNames[0], ca.config.Address[strings.LastIndex(ca.config.Address, ":"):]) @@ -456,13 +471,13 @@ func (ca *CA) Reload() error { return nil } -// getTLSConfig returns a TLSConfig for the CA server with a self-renewing -// server certificate. -func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) { +// get TLSConfig returns separate TLSConfigs for server and client with the +// same self-renewing certificate. +func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config, error) { // Create initial TLS certificate tlsCrt, err := auth.GetTLSCertificate() if err != nil { - return nil, err + return nil, nil, err } // Start tls renewer with the new certificate. @@ -473,15 +488,15 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) { ca.renewer, err = NewTLSRenewer(tlsCrt, auth.GetTLSCertificate) if err != nil { - return nil, err + return nil, nil, err } ca.renewer.Run() - var tlsConfig *tls.Config + var serverTLSConfig *tls.Config if ca.config.TLS != nil { - tlsConfig = ca.config.TLS.TLSConfig() + serverTLSConfig = ca.config.TLS.TLSConfig() } else { - tlsConfig = &tls.Config{ + serverTLSConfig = &tls.Config{ MinVersion: tls.VersionTLS12, } } @@ -493,13 +508,24 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) { // first entry in the Certificates attribute; by setting the attribute to // empty we are implicitly forcing GetCertificate to be the only mechanism // by which the server can find it's own leaf Certificate. - tlsConfig.Certificates = []tls.Certificate{} - tlsConfig.GetCertificate = ca.renewer.GetCertificateForCA + serverTLSConfig.Certificates = []tls.Certificate{} + + clientTLSConfig := serverTLSConfig.Clone() + + serverTLSConfig.GetCertificate = ca.renewer.GetCertificateForCA + clientTLSConfig.GetClientCertificate = ca.renewer.GetClientCertificate // initialize a certificate pool with root CA certificates to trust when doing mTLS. certPool := x509.NewCertPool() + // initialize a certificate pool with root CA certificates to trust when connecting + // to webhook servers + rootCAsPool, err := x509.SystemCertPool() + if err != nil { + return nil, nil, err + } for _, crt := range auth.GetRootCertificates() { certPool.AddCert(crt) + rootCAsPool.AddCert(crt) } // adding the intermediate CA certificates to the pool will allow clients that @@ -509,16 +535,19 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, error) { for _, certBytes := range intermediates { cert, err := x509.ParseCertificate(certBytes) if err != nil { - return nil, err + return nil, nil, err } certPool.AddCert(cert) + rootCAsPool.AddCert(cert) } // Add support for mutual tls to renew certificates - tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven - tlsConfig.ClientCAs = certPool + serverTLSConfig.ClientAuth = tls.VerifyClientCertIfGiven + serverTLSConfig.ClientCAs = certPool - return tlsConfig, nil + clientTLSConfig.RootCAs = rootCAsPool + + return serverTLSConfig, clientTLSConfig, nil } // shouldServeSCEPEndpoints returns if the CA should be @@ -540,3 +569,10 @@ func dumpRoutes(mux chi.Routes) { fmt.Printf("Logging err: %s\n", err.Error()) } } + +func (ca *CA) getConfigFileOutput() string { + if ca.config.WasLoadedFromFile() { + return ca.config.Filepath() + } + return "loaded from token" +} diff --git a/ca/client_test.go b/ca/client_test.go index 48aa1488..dff7fd41 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -519,9 +519,10 @@ func TestClient_Renew(t *testing.T) { t.Errorf("Client.Renew() = %v, want nil", got) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, err.Error(), tt.err.Error()) default: if !reflect.DeepEqual(got, tt.response) { @@ -587,9 +588,10 @@ func TestClient_RenewWithToken(t *testing.T) { t.Errorf("Client.RenewWithToken() = %v, want nil", got) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, err.Error(), tt.err.Error()) default: if !reflect.DeepEqual(got, tt.response) { @@ -656,9 +658,10 @@ func TestClient_Rekey(t *testing.T) { t.Errorf("Client.Renew() = %v, want nil", got) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, err.Error(), tt.err.Error()) default: if !reflect.DeepEqual(got, tt.response) { @@ -777,9 +780,10 @@ func TestClient_ProvisionerKey(t *testing.T) { t.Errorf("Client.ProvisionerKey() = %v, want nil", got) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { @@ -836,9 +840,10 @@ func TestClient_Roots(t *testing.T) { if got != nil { t.Errorf("Client.Roots() = %v, want nil", got) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, err.Error(), tt.err.Error()) default: if !reflect.DeepEqual(got, tt.response) { @@ -894,9 +899,10 @@ func TestClient_Federation(t *testing.T) { if got != nil { t.Errorf("Client.Federation() = %v, want nil", got) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { @@ -956,9 +962,10 @@ func TestClient_SSHRoots(t *testing.T) { if got != nil { t.Errorf("Client.SSHKeys() = %v, want nil", got) } - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, tt.err.Error(), err.Error()) default: if !reflect.DeepEqual(got, tt.response) { @@ -1118,9 +1125,10 @@ func TestClient_SSHBastion(t *testing.T) { t.Errorf("Client.SSHBastion() = %v, want nil", got) } if tt.responseCode != 200 { - sc, ok := err.(render.StatusCodedError) - assert.Fatal(t, ok, "error does not implement StatusCodedError interface") - assert.Equals(t, sc.StatusCode(), tt.responseCode) + var sc render.StatusCodedError + if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") { + assert.Equals(t, sc.StatusCode(), tt.responseCode) + } assert.HasPrefix(t, err.Error(), tt.err.Error()) } default: diff --git a/docker/Dockerfile.step-ca b/docker/Dockerfile.step-ca index 46677a91..ed6b5f56 100644 --- a/docker/Dockerfile.step-ca +++ b/docker/Dockerfile.step-ca @@ -4,6 +4,7 @@ WORKDIR /src COPY . . RUN apk add --no-cache curl git make +RUN make V=1 download RUN make V=1 bin/step-ca bin/step-awskms-init bin/step-cloudkms-init diff --git a/docker/Dockerfile.step-ca.hsm b/docker/Dockerfile.step-ca.hsm index ac59c909..8f413cd7 100644 --- a/docker/Dockerfile.step-ca.hsm +++ b/docker/Dockerfile.step-ca.hsm @@ -5,6 +5,7 @@ COPY . . RUN apk add --no-cache curl git make RUN apk add --no-cache gcc musl-dev pkgconf pcsc-lite-dev +RUN make V=1 download RUN make V=1 GOFLAGS="" build diff --git a/docs/README.md b/docs/README.md index 5ef683ca..95e4700b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,7 +28,7 @@ Index of Documentation and Tutorials for using and deploying the `step certifica * Check out our [Blog](https://smallstep.com/blog/). We post quality educational content as well as periodic updates on new releases. * **API**: Guides to using the API via the `step` CLI. - * [Revoking Certificates](https://smallstep.com/docs/step-ca/certificate-authority-server-production#x509-certificate-revocation) + * [Revoking Certificates](https://smallstep.com/docs/step-ca/revocation) * [Persistence Layer](https://smallstep.com/docs/step-ca/configuration#databases): description and guide to using `step certificates`' persistence layer for storing certificate management metadata. * **Tutorials**: Guides for deploying and getting started with `step` in various environments. diff --git a/errs/error.go b/errs/error.go index 14995f0a..ba066925 100644 --- a/errs/error.go +++ b/errs/error.go @@ -143,8 +143,9 @@ func (e *Error) UnmarshalJSON(data []byte) error { // Format implements the fmt.Formatter interface. func (e *Error) Format(f fmt.State, c rune) { - if err, ok := e.Err.(fmt.Formatter); ok { - err.Format(f, c) + var fe fmt.Formatter + if errors.As(e.Err, &fe) { + fe.Format(f, c) return } fmt.Fprint(f, e.Err.Error()) @@ -253,7 +254,8 @@ func NewError(status int, err error, format string, args ...interface{}) error { return err } msg := fmt.Sprintf(format, args...) - if _, ok := err.(log.StackTracedError); !ok { + var ste log.StackTracedError + if !errors.As(err, &ste) { err = errors.Wrap(err, msg) } return &Error{ @@ -268,15 +270,11 @@ func NewError(status int, err error, format string, args ...interface{}) error { func NewErr(status int, err error, opts ...Option) error { var e *Error if !errors.As(err, &e) { - if sc, ok := err.(render.StatusCodedError); ok { - e = &Error{Status: sc.StatusCode(), Err: err} + var ste render.StatusCodedError + if errors.As(err, &ste) { + e = &Error{Status: ste.StatusCode(), Err: err} } else { - cause := errors.Cause(err) - if sc, ok := cause.(render.StatusCodedError); ok { - e = &Error{Status: sc.StatusCode(), Err: err} - } else { - e = &Error{Status: status, Err: err} - } + e = &Error{Status: status, Err: err} } } for _, o := range opts { diff --git a/go.mod b/go.mod index 1ba86c92..21d1f557 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,16 @@ module github.com/smallstep/certificates go 1.18 require ( - cloud.google.com/go v0.102.1 - cloud.google.com/go/security v1.7.0 + cloud.google.com/go v0.104.0 + cloud.google.com/go/security v1.8.0 github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.27 // indirect + github.com/Azure/go-autorest/autorest v0.11.28 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Masterminds/sprig/v3 v3.2.2 github.com/ThalesIgnite/crypto11 v1.2.5 // indirect - github.com/aws/aws-sdk-go v1.44.37 // indirect - github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect + github.com/aws/aws-sdk-go v1.44.111 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/fatih/color v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 github.com/go-chi/chi v4.1.2+incompatible @@ -20,46 +20,46 @@ require ( github.com/go-piv/piv-go v1.10.0 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/golang/mock v1.6.0 - github.com/google/go-cmp v0.5.8 + github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 - github.com/googleapis/gax-go/v2 v2.4.0 - github.com/hashicorp/vault/api v1.3.1 - github.com/hashicorp/vault/api/auth/approle v0.1.1 - github.com/hashicorp/vault/api/auth/kubernetes v0.1.0 + github.com/googleapis/gax-go/v2 v2.6.0 + github.com/hashicorp/vault/api v1.8.1 + github.com/hashicorp/vault/api/auth/approle v0.3.0 + github.com/hashicorp/vault/api/auth/kubernetes v0.3.0 github.com/jhump/protoreflect v1.9.0 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.13 // indirect github.com/micromdm/scep/v2 v2.1.0 - github.com/newrelic/go-agent/v3 v3.18.0 + github.com/newrelic/go-agent/v3 v3.19.2 github.com/pkg/errors v0.9.1 - github.com/rs/xid v1.2.1 - github.com/sirupsen/logrus v1.8.1 - github.com/slackhq/nebula v1.5.2 + github.com/rs/xid v1.4.0 + github.com/sirupsen/logrus v1.9.0 + github.com/slackhq/nebula v1.6.1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 - github.com/smallstep/nosql v0.4.0 - github.com/stretchr/testify v1.8.0 - github.com/urfave/cli v1.22.4 + github.com/smallstep/nosql v0.5.0 + github.com/stretchr/testify v1.8.1 + github.com/urfave/cli v1.22.10 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 - go.step.sm/cli-utils v0.7.4 - go.step.sm/crypto v0.19.0 - go.step.sm/linkedca v0.19.0-rc.2 - golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 - golang.org/x/net v0.0.0-20220927171203-f486391704dc - golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect + go.step.sm/cli-utils v0.7.5 + go.step.sm/crypto v0.21.0 + go.step.sm/linkedca v0.19.0-rc.3 + golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b + golang.org/x/net v0.0.0-20221014081412-f15817d10f9b + golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect - google.golang.org/api v0.96.0 - google.golang.org/genproto v0.0.0-20220929141241-1ce7b20da813 - google.golang.org/grpc v1.49.0 + google.golang.org/api v0.100.0 + google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a + google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 gopkg.in/square/go-jose.v2 v2.6.0 ) require ( - cloud.google.com/go/compute v1.7.0 // indirect + cloud.google.com/go/compute v1.10.0 // indirect cloud.google.com/go/iam v0.3.0 // indirect cloud.google.com/go/kms v1.4.0 // indirect - filippo.io/edwards25519 v1.0.0-rc.1 // indirect + filippo.io/edwards25519 v1.0.0 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect @@ -74,6 +74,7 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -82,12 +83,13 @@ require ( github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/go-logfmt/logfmt v0.5.0 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v0.16.2 // indirect @@ -97,34 +99,34 @@ require ( github.com/hashicorp/go-retryablehttp v0.6.6 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 // indirect - github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-version v1.2.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/vault/sdk v0.3.0 // indirect + github.com/hashicorp/vault/sdk v0.6.0 // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.10.1 // indirect + github.com/jackc/pgconn v1.13.0 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.2.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.1 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.9.0 // indirect - github.com/jackc/pgx/v4 v4.14.0 // indirect + github.com/jackc/pgtype v1.12.0 // indirect + github.com/jackc/pgx/v4 v4.17.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.12.3 // indirect + github.com/klauspost/compress v1.15.11 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect @@ -139,16 +141,18 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect - golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect + golang.org/x/text v0.3.8 // indirect google.golang.org/appengine v1.6.7 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto + // replace go.step.sm/cli-utils => ../cli-utils // 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-20211016004704-52592125d6f6 +replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20221024180420-e1aab68dda05 diff --git a/go.sum b/go.sum index 75f369cd..e50444bb 100644 --- a/go.sum +++ b/go.sum @@ -28,9 +28,8 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1 h1:vpK6iQWv/2uUeFJth4/cBHsQAGjn1iIE6AAlxipRaA0= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= 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= @@ -39,11 +38,8 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0 h1:aoLIYaA1fX3ywihqpBk2APQKOo20nXsp1GEZQbx5Jk4= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= @@ -55,17 +51,16 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k 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/security v1.7.0 h1:176N+6wf67OA6HgqhmNN/AfmUtwq50na2VKR6/6l34k= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0 h1:linnRc3/gJYDfKbAtNixVQ52+66DpOx5MmCz0NNxal8= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= -filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw= @@ -73,8 +68,8 @@ github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= -github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= +github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= @@ -133,8 +128,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.44.37 h1:KvDxCX6dfJeEDC77U5GPGSP0ErecmNnhDHFxw+NIvlI= -github.com/aws/aws-sdk-go v1.44.37/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.111 h1:AcWfOgeedSQ4gQVwcIe6aLxpQNJMloZQyqnr7Dzki+s= +github.com/aws/aws-sdk-go v1.44.111/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -149,6 +144,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -166,7 +163,6 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -194,8 +190,8 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= -github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -218,7 +214,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -250,8 +245,9 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-piv/piv-go v1.10.0 h1:P1Y1VjBI5DnXW0+YkKmTuh5opWnMIrKriUaIOblee9Q= github.com/go-piv/piv-go v1.10.0/go.mod h1:NZ2zmjVkfFaL/CF8cVQ/pXdXtuj110zEKGdJM6fJZZM= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -273,6 +269,8 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -326,8 +324,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -353,18 +351,14 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= 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.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= 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/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -394,7 +388,7 @@ github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0/go.mod h1:xvb32K2keAc+R8DSFG2IwDcydK9DBQE+fGA5fsw6hSk= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -410,11 +404,13 @@ github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3 github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= @@ -437,15 +433,15 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= -github.com/hashicorp/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= -github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= -github.com/hashicorp/vault/api/auth/approle v0.1.1 h1:R5yA+xcNvw1ix6bDuWOaLOq2L4L77zDCVsethNw97xQ= -github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag= -github.com/hashicorp/vault/api/auth/kubernetes v0.1.0 h1:6BtyahbF4aQp8gg3ww0A/oIoqzbhpNP1spXU3nHE0n0= -github.com/hashicorp/vault/api/auth/kubernetes v0.1.0/go.mod h1:Pdgk78uIs0mgDOLvc3a+h/vYIT9rznw2sz+ucuH9024= -github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY= -github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= +github.com/hashicorp/vault/api v1.8.0/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= +github.com/hashicorp/vault/api v1.8.1 h1:bMieWIe6dAlqAAPReZO/8zYtXaWUg/21umwqGZpEjCI= +github.com/hashicorp/vault/api v1.8.1/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= +github.com/hashicorp/vault/api/auth/approle v0.3.0 h1:Ib0oCNXsCq/QZhPYtXPzJEbGS5WR/KoZf8c84QoFdkU= +github.com/hashicorp/vault/api/auth/approle v0.3.0/go.mod h1:hm51TbjzUkPO0Y17wkrpwOpvyyMRpXJNueTHiG04t3k= +github.com/hashicorp/vault/api/auth/kubernetes v0.3.0 h1:HkaCmTKzcgLa2tjdiAid1rbmyQNmQGHfnmvIIM2WorY= +github.com/hashicorp/vault/api/auth/kubernetes v0.3.0/go.mod h1:l1B4MGtLc+P37MabBQiIhP3qd9agj0vqhETmaQjjC/Y= +github.com/hashicorp/vault/sdk v0.6.0 h1:6Z+In5DXHiUfZvIZdMx7e2loL1PPyDjA4bVh9ZTIAhs= +github.com/hashicorp/vault/sdk v0.6.0/go.mod h1:+DRpzoXIdMvKc88R4qxr+edwy/RvH5QK8itmxLiDHLc= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -471,8 +467,9 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= +github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -488,26 +485,30 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= +github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.9.0 h1:/SH1RxEtltvJgsDqp3TbiTFApD3mey3iygpuEGeuBXk= github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= +github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.14.0 h1:TgdrmgnM7VY72EuSQzBbBd4JA1RLqJolrw9nQVZABVc= github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= +github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= +github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w= @@ -528,8 +529,9 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -596,8 +598,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -613,8 +615,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.18.0 h1:AOR3hhF2ZVE0yfvNPuOaEhEvNMYyIfEBY8EizQpnt7g= -github.com/newrelic/go-agent/v3 v3.18.0/go.mod h1:BFJOlbZWRlPTXKYIC1TTTtQKTnYntEJaU0VU507hDc0= +github.com/newrelic/go-agent/v3 v3.19.2 h1:gu5Vyp/61gMzXptr+va7+pliKaRkNShoSadYG8dM0IQ= +github.com/newrelic/go-agent/v3 v3.19.2/go.mod h1:rT6ZUxJc5rQbWLyCtjqQCOcfb01lKRFbc1yMQkcboWM= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= @@ -676,8 +678,9 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -698,17 +701,17 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/slackhq/nebula v1.5.2 h1:wuIOHsOnrNw3rQx8yPxXiGu8wAtAxxtUI/K8W7Vj7EI= -github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIpR52WlE= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/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-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/nosql v0.4.0 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M= -github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= -github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6 h1:8Rjy6IZbSM/jcYgBWCoLIGjug7QcoLtF9sUuhDrHD2U= -github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +github.com/smallstep/nosql v0.5.0 h1:1BPyHy8bha8qSaxgULGEdqhXpNFXimAfudnauFVqmxw= +github.com/smallstep/nosql v0.5.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= +github.com/smallstep/pkcs7 v0.0.0-20221024180420-e1aab68dda05 h1:nVZXaJTwrUcfPUSZknkOidfITqOXSO0wE8pkOUTOdSM= +github.com/smallstep/pkcs7 v0.0.0-20221024180420-e1aab68dda05/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -733,8 +736,9 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -742,8 +746,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -752,8 +757,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= -github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 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/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -780,13 +785,13 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.step.sm/cli-utils v0.7.4 h1:oI7PStZqlvjPZ0u2EB4lN7yZ4R3ShTotdGL/L84Oorg= -go.step.sm/cli-utils v0.7.4/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I= +go.step.sm/cli-utils v0.7.5 h1:jyp6X8k8mN1B0uWJydTid0C++8tQhm2kaaAdXKQQzdk= +go.step.sm/cli-utils v0.7.5/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/crypto v0.19.0 h1:WxjUDeTDpuPZ1IR3v6c4jc6WdlQlS5IYYQBhfnG5uW0= -go.step.sm/crypto v0.19.0/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw= -go.step.sm/linkedca v0.19.0-rc.2 h1:IcPqZ5y7MZNq1+VbYQcKoQEvX80NKRncU1WFCDyY+So= -go.step.sm/linkedca v0.19.0-rc.2/go.mod h1:MCZmPIdzElEofZbiw4eyUHayXgFTwa94cNAV34aJ5ew= +go.step.sm/crypto v0.21.0 h1:Qwxk5JrqG0Q1t8tOfDM3zKTNECG6J5J24qgWZCRM0Ic= +go.step.sm/crypto v0.21.0/go.mod h1:diT2XWIHQy0397UI3i78qCKeLLLp2wu0/DIJI66u/MU= +go.step.sm/linkedca v0.19.0-rc.3 h1:3Uu8j187wm7mby+/pz/aQ0wHKRm7w/2AsVPpvcAn4v8= +go.step.sm/linkedca v0.19.0-rc.3/go.mod h1:MCZmPIdzElEofZbiw4eyUHayXgFTwa94cNAV34aJ5ew= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -818,8 +823,9 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -902,15 +908,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= -golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -927,13 +926,8 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= 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= @@ -945,7 +939,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1018,16 +1011,9 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM= +golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= @@ -1041,8 +1027,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b h1:NXqSWXSRUSCaFuvitrWtU169I3876zRTalMRbfd6LL0= -golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1117,9 +1103,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -1154,14 +1137,8 @@ google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3h google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.100.0 h1:LGUYIrbW9pzYQQ8NWXlaIVkgnfubVBZbMFb9P8TK374= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= 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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1212,7 +1189,6 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1238,23 +1214,8 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220929141241-1ce7b20da813 h1:buul04Ikd79A5tP8nGhKEyMfr+/HplsO6nqSUapWZ/M= -google.golang.org/genproto v0.0.0-20220929141241-1ce7b20da813/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a h1:GH6UPn3ixhWcKDhpnEC55S75cerLPdpp3hrhfKYjZgw= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= 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= @@ -1289,12 +1250,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= 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= @@ -1310,14 +1267,14 @@ google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX7 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/pki/helm.go b/pki/helm.go index e13bb97c..72d95971 100644 --- a/pki/helm.go +++ b/pki/helm.go @@ -17,6 +17,7 @@ type helmVariables struct { Defaults *linkedca.Defaults Password string EnableSSH bool + EnableAdmin bool TLS authconfig.TLSOptions Provisioners []provisioner.Interface } @@ -34,14 +35,39 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error { p.Ssh = nil } - // Convert provisioner to ca.json - provisioners := make([]provisioner.Interface, len(p.Authority.Provisioners)) - for i, p := range p.Authority.Provisioners { + // Convert provisioners to ca.json representation + provisioners := []provisioner.Interface{} + for _, p := range p.Authority.Provisioners { pp, err := authority.ProvisionerToCertificates(p) if err != nil { return err } - provisioners[i] = pp + provisioners = append(provisioners, pp) + } + + // Add default ACME provisioner if enabled. Note that this logic is similar + // to what's in p.GenerateConfig(), but that codepath isn't taken when + // writing the Helm template. The default JWK provisioner is added earlier in + // the process and that's part of the provisioners above. + // TODO(hs): consider refactoring the initialization, so that this becomes + // easier to reason about and maintain. + if p.options.enableACME { + provisioners = append(provisioners, &provisioner.ACME{ + Type: "ACME", + Name: "acme", + }) + } + + // Add default SSHPOP provisioner if enabled. Similar to the above, this is + // the same as what happens in p.GenerateConfig(). + if p.options.enableSSH { + provisioners = append(provisioners, &provisioner.SSHPOP{ + Type: "SSHPOP", + Name: "sshpop", + Claims: &provisioner.Claims{ + EnableSSHCA: &p.options.enableSSH, + }, + }) } if err := tmpl.Execute(w, helmVariables{ @@ -49,6 +75,7 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error { Defaults: &p.Defaults, Password: "", EnableSSH: p.options.enableSSH, + EnableAdmin: p.options.enableAdmin, TLS: authconfig.DefaultTLSOptions, Provisioners: provisioners, }); err != nil { @@ -88,6 +115,7 @@ inject: type: badgerv2 dataSource: /home/step/db authority: + enableAdmin: {{ .EnableAdmin }} provisioners: {{- range .Provisioners }} - {{ . | toJson }} diff --git a/pki/helm_test.go b/pki/helm_test.go new file mode 100644 index 00000000..ea1c4acd --- /dev/null +++ b/pki/helm_test.go @@ -0,0 +1,232 @@ +package pki + +import ( + "bytes" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + + "go.step.sm/crypto/jose" + "go.step.sm/linkedca" + + "github.com/smallstep/certificates/cas/apiv1" +) + +func TestPKI_WriteHelmTemplate(t *testing.T) { + var preparePKI = func(t *testing.T, opts ...Option) *PKI { + o := apiv1.Options{ + Type: "softcas", + IsCreator: true, + } + + // Add default WithHelm option + opts = append(opts, WithHelm()) + + // TODO(hs): invoking `New` doesn't perform all operations that are executed + // when `ca init --helm` is executed. Ideally this logic should be handled + // in one place and probably inside of the PKI initialization. For testing + // purposes the missing operations to fill a Helm template fully are faked + // by `setKeyPair`, `setCertificates` and `setSSHSigningKeys` + p, err := New(o, opts...) + assert.NoError(t, err) + + // setKeyPair sets a predefined JWK and a default JWK provisioner. This is one + // of the things performed in the `ca init` code that's not part of `New`, but + // performed after that in p.GenerateKeyPairs`. We're currently using the same + // JWK for every test to keep test variance small: we're not testing JWK generation + // here after all. It's a bit dangerous to redefine the function here, but it's + // the simplest way to make this fully testable without refactoring the init now. + // The password for the predefined encrypted key is \x01\x03\x03\x07. + setKeyPair(t, p) + + // setCertificates sets some static intermediate and root CA certificate bytes. It + // replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`, + // and `p.GenerateIntermediateCertificate`. + setCertificates(t, p) + + // setSSHSigningKeys sets predefined SSH user and host certificate and key bytes. + // This replaces the logic in `p.GenerateSSHSigningKeys` + setSSHSigningKeys(t, p) + + return p + } + type test struct { + pki *PKI + testFile string + wantErr bool + } + var tests = map[string]func(t *testing.T) test{ + "ok/simple": func(t *testing.T) test { + return test{ + pki: preparePKI(t), + testFile: "testdata/helm/simple.yml", + wantErr: false, + } + }, + "ok/with-provisioner": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithProvisioner("a-provisioner")), + testFile: "testdata/helm/with-provisioner.yml", + wantErr: false, + } + }, + "ok/with-acme": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithACME()), + testFile: "testdata/helm/with-acme.yml", + wantErr: false, + } + }, + "ok/with-admin": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithAdmin()), + testFile: "testdata/helm/with-admin.yml", + wantErr: false, + } + }, + "ok/with-ssh": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithSSH()), + testFile: "testdata/helm/with-ssh.yml", + wantErr: false, + } + }, + "ok/with-ssh-and-acme": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithSSH(), WithACME()), + testFile: "testdata/helm/with-ssh-and-acme.yml", + wantErr: false, + } + }, + "fail/authority.ProvisionerToCertificates": func(t *testing.T) test { + pki := preparePKI(t) + pki.Authority.Provisioners = append(pki.Authority.Provisioners, + &linkedca.Provisioner{ + Type: linkedca.Provisioner_JWK, + Name: "Broken JWK", + Details: nil, + }, + ) + return test{ + pki: pki, + wantErr: true, + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + + w := &bytes.Buffer{} + if err := tc.pki.WriteHelmTemplate(w); (err != nil) != tc.wantErr { + t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tc.wantErr) + return + } + + if tc.wantErr { + // don't compare output if an error was expected on output + return + } + + wantBytes, err := os.ReadFile(tc.testFile) + assert.NoError(t, err) + if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" { + t.Logf("Generated Helm template did not match reference %q\n", tc.testFile) + t.Errorf("Diff follows:\n%s\n", diff) + t.Errorf("Full output:\n%s\n", w.Bytes()) + } + }) + } +} + +// setKeyPair sets a predefined JWK and a default JWK provisioner. +func setKeyPair(t *testing.T, p *PKI) { + t.Helper() + + var err error + + p.ottPublicKey, err = jose.ParseKey([]byte(`{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"}`)) + if err != nil { + t.Fatal(err) + } + + p.ottPrivateKey, err = jose.ParseEncrypted("eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA") + if err != nil { + t.Fatal(err) + } + + var claims *linkedca.Claims + if p.options.enableSSH { + claims = &linkedca.Claims{ + Ssh: &linkedca.SSHClaims{ + Enabled: true, + }, + } + } + + publicKey, err := json.Marshal(p.ottPublicKey) + if err != nil { + t.Fatal(err) + } + encryptedKey, err := p.ottPrivateKey.CompactSerialize() + if err != nil { + t.Fatal(err) + } + p.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{ + Type: linkedca.Provisioner_JWK, + Name: p.options.provisioner, + Claims: claims, + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_JWK{ + JWK: &linkedca.JWKProvisioner{ + PublicKey: publicKey, + EncryptedPrivateKey: []byte(encryptedKey), + }, + }, + }, + }) +} + +// setCertificates sets some static, gibberish intermediate and root CA certificate and key bytes. +func setCertificates(t *testing.T, p *PKI) { + raw := []byte("these are just some fake root CA cert bytes") + p.Files[p.Root[0]] = encodeCertificate(&x509.Certificate{Raw: raw}) + p.Files[p.RootKey[0]] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("these are just some fake root CA key bytes"), + }) + p.Files[p.Intermediate] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake intermediate CA cert bytes")}) + p.Files[p.IntermediateKey] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("these are just some fake intermediate CA key bytes"), + }) + sum := sha256.Sum256(raw) + p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:])) +} + +// setSSHSigningKeys sets some static, gibberish ssh user and host CA certificate and key bytes. +func setSSHSigningKeys(t *testing.T, p *PKI) { + + if !p.options.enableSSH { + return + } + + p.Files[p.Ssh.HostKey] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("fake ssh host key bytes"), + }) + p.Files[p.Ssh.HostPublicKey] = []byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=") + p.Files[p.Ssh.UserKey] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("fake ssh user key bytes"), + }) + p.Files[p.Ssh.UserPublicKey] = []byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=") +} diff --git a/pki/pki.go b/pki/pki.go index c05eadbd..d6c15c9e 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -176,6 +176,7 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { type options struct { provisioner string + superAdminSubject string pkiOnly bool enableACME bool enableSSH bool @@ -220,6 +221,15 @@ func WithProvisioner(s string) Option { } } +// WithSuperAdminSubject defines the subject of the first +// super admin for use with the Admin API. The admin will belong +// to the first JWK provisioner. +func WithSuperAdminSubject(s string) Option { + return func(p *PKI) { + p.options.superAdminSubject = s + } +} + // WithPKIOnly will only generate the PKI without the step-ca config files. func WithPKIOnly() Option { return func(p *PKI) { @@ -307,6 +317,9 @@ type PKI struct { // New creates a new PKI configuration. func New(o apiv1.Options, opts ...Option) (*PKI, error) { + // TODO(hs): invoking `New` with a context active will use values from + // that CA context while generating the context. Thay may or may not + // be fully expected and/or what we want. Check that. currentCtx := step.Contexts().GetCurrent() caService, err := cas.New(context.Background(), o) if err != nil { @@ -645,7 +658,7 @@ func (p *PKI) GetCertificateAuthority() error { // SSH user certificates and a private key used for signing host certificates. func (p *PKI) GenerateSSHSigningKeys(password []byte) error { // Enable SSH - p.options.enableSSH = true + p.options.enableSSH = true // TODO(hs): change this function to not mutate configuration state // Create SSH key used to sign host certificates. Using // kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm. @@ -883,6 +896,11 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) { // // Note that we might want to be able to define the database as a // flag in `step ca init` so we can write to the proper place. + // + // TODO(hs): the logic for creating the provisioners and the super admin + // is similar to what's done when automatically migrating the provisioners. + // This is related to the existing comment above. Refactor this to exist in + // a single place and ensure it happens only once. _db, err := db.New(cfg.DB) if err != nil { return nil, err @@ -906,9 +924,13 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) { } } // Add the first provisioner as an admin. + superAdminSubject := "step" + if p.options.superAdminSubject != "" { + superAdminSubject = p.options.superAdminSubject + } if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{ AuthorityId: admin.DefaultAuthorityID, - Subject: "step", + Subject: superAdminSubject, Type: linkedca.Admin_SUPER_ADMIN, ProvisionerId: adminID, }); err != nil { @@ -991,6 +1013,18 @@ func (p *PKI) Save(opt ...ConfigOption) error { ui.PrintSelected("Default profile configuration", p.profileDefaults) } ui.PrintSelected("Certificate Authority configuration", p.config) + if cfg.AuthorityConfig.EnableAdmin && p.options.deploymentType != LinkedDeployment { + // TODO(hs): we may want to get this information from the DB, because that's + // where the admin and provisioner are stored in this case. Requires some + // refactoring. + superAdminSubject := "step" + if p.options.superAdminSubject != "" { + superAdminSubject = p.options.superAdminSubject + } + ui.PrintSelected("Admin provisioner", fmt.Sprintf("%s (JWK)", p.options.provisioner)) + ui.PrintSelected("Super admin subject", superAdminSubject) + } + if p.options.deploymentType != LinkedDeployment { ui.Println() if p.casOptions.Is(apiv1.SoftCAS) { diff --git a/pki/testdata/helm/simple.yml b/pki/testdata/helm/simple.yml new file mode 100644 index 00000000..8a7e369f --- /dev/null +++ b/pki/testdata/helm/simple.yml @@ -0,0 +1,81 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- + + + # root_ca contains the text of the root CA Certificate + root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-acme.yml b/pki/testdata/helm/with-acme.yml new file mode 100644 index 00000000..488bc32f --- /dev/null +++ b/pki/testdata/helm/with-acme.yml @@ -0,0 +1,82 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} + - {"type":"ACME","name":"acme"} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- + + + # root_ca contains the text of the root CA Certificate + root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-admin.yml b/pki/testdata/helm/with-admin.yml new file mode 100644 index 00000000..790fbdd4 --- /dev/null +++ b/pki/testdata/helm/with-admin.yml @@ -0,0 +1,81 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: true + provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- + + + # root_ca contains the text of the root CA Certificate + root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-provisioner.yml b/pki/testdata/helm/with-provisioner.yml new file mode 100644 index 00000000..de17ef0a --- /dev/null +++ b/pki/testdata/helm/with-provisioner.yml @@ -0,0 +1,81 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"JWK","name":"a-provisioner","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- + + + # root_ca contains the text of the root CA Certificate + root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-ssh-and-acme.yml b/pki/testdata/helm/with-ssh-and-acme.yml new file mode 100644 index 00000000..639aca6a --- /dev/null +++ b/pki/testdata/helm/with-ssh-and-acme.yml @@ -0,0 +1,105 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + ssh: + hostKey: /home/step/secrets/ssh_host_ca_key + userKey: /home/step/secrets/ssh_user_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false},"options":{"x509":{},"ssh":{}}} + - {"type":"ACME","name":"acme"} + - {"type":"SSHPOP","name":"sshpop","claims":{"enableSSHCA":true}} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- + + + # root_ca contains the text of the root CA Certificate + root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- + + # ssh_host_ca contains the text of the public ssh key for the SSH root CA + ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms= + + # ssh_user_ca contains the text of the public ssh key for the SSH root CA + ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs= + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + + ssh: + # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key + host_ca_key: | + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + + + # ssh_user_ca_key contains the contents of your encrypted SSH User CA key + user_ca_key: | + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-ssh.yml b/pki/testdata/helm/with-ssh.yml new file mode 100644 index 00000000..2e4845f0 --- /dev/null +++ b/pki/testdata/helm/with-ssh.yml @@ -0,0 +1,104 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + ssh: + hostKey: /home/step/secrets/ssh_host_ca_key + userKey: /home/step/secrets/ssh_user_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false},"options":{"x509":{},"ssh":{}}} + - {"type":"SSHPOP","name":"sshpop","claims":{"enableSSHCA":true}} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- + + + # root_ca contains the text of the root CA Certificate + root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- + + # ssh_host_ca contains the text of the public ssh key for the SSH root CA + ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms= + + # ssh_user_ca contains the text of the public ssh key for the SSH root CA + ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs= + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + + ssh: + # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key + host_ca_key: | + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + + + # ssh_user_ca_key contains the contents of your encrypted SSH User CA key + user_ca_key: | + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + diff --git a/scep/authority.go b/scep/authority.go index bdba1d5f..585b937e 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -281,6 +281,14 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m if err != nil { return nil, fmt.Errorf("error retrieving authorization options from SCEP provisioner: %w", err) } + // Unlike most of the provisioners, scep's AuthorizeSign method doesn't + // define the templates, and the template data used in WebHooks is not + // available. + for _, signOp := range signOps { + if wc, ok := signOp.(*provisioner.WebhookController); ok { + wc.TemplateData = data + } + } opts := provisioner.SignOptions{} templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) diff --git a/webhook/options.go b/webhook/options.go new file mode 100644 index 00000000..88c44986 --- /dev/null +++ b/webhook/options.go @@ -0,0 +1,97 @@ +package webhook + +import ( + "crypto/x509" + + "go.step.sm/crypto/sshutil" + "go.step.sm/crypto/x509util" + "golang.org/x/crypto/ssh" +) + +type RequestBodyOption func(*RequestBody) error + +func NewRequestBody(options ...RequestBodyOption) (*RequestBody, error) { + rb := &RequestBody{} + + for _, fn := range options { + if err := fn(rb); err != nil { + return nil, err + } + } + + return rb, nil +} + +func WithX509CertificateRequest(cr *x509.CertificateRequest) RequestBodyOption { + return func(rb *RequestBody) error { + rb.X509CertificateRequest = &X509CertificateRequest{ + CertificateRequest: x509util.NewCertificateRequestFromX509(cr), + PublicKeyAlgorithm: cr.PublicKeyAlgorithm.String(), + Raw: cr.Raw, + } + if cr.PublicKey != nil { + key, err := x509.MarshalPKIXPublicKey(cr.PublicKey) + if err != nil { + return err + } + rb.X509CertificateRequest.PublicKey = key + } + + return nil + } +} + +func WithX509Certificate(cert *x509util.Certificate, leaf *x509.Certificate) RequestBodyOption { + return func(rb *RequestBody) error { + rb.X509Certificate = &X509Certificate{ + Certificate: cert, + 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.X509Certificate.PublicKey = key + } + + return nil + } +} + +func WithAttestationData(data *AttestationData) RequestBodyOption { + return func(rb *RequestBody) error { + rb.AttestationData = data + return nil + } +} + +func WithSSHCertificateRequest(cr sshutil.CertificateRequest) RequestBodyOption { + return func(rb *RequestBody) error { + rb.SSHCertificateRequest = &SSHCertificateRequest{ + Type: cr.Type, + KeyID: cr.KeyID, + Principals: cr.Principals, + } + if cr.Key != nil { + rb.SSHCertificateRequest.PublicKey = cr.Key.Marshal() + } + return nil + } +} + +func WithSSHCertificate(cert *sshutil.Certificate, certTpl *ssh.Certificate) RequestBodyOption { + return func(rb *RequestBody) error { + rb.SSHCertificate = &SSHCertificate{ + Certificate: cert, + ValidBefore: certTpl.ValidBefore, + ValidAfter: certTpl.ValidAfter, + } + if certTpl.Key != nil { + rb.SSHCertificate.PublicKey = certTpl.Key.Marshal() + } + return nil + } +} diff --git a/webhook/options_test.go b/webhook/options_test.go new file mode 100644 index 00000000..e813bb44 --- /dev/null +++ b/webhook/options_test.go @@ -0,0 +1,116 @@ +package webhook + +import ( + "crypto/x509" + "crypto/x509/pkix" + "testing" + "time" + + "github.com/smallstep/assert" + "go.step.sm/crypto/sshutil" + "go.step.sm/crypto/x509util" + "golang.org/x/crypto/ssh" +) + +func TestNewRequestBody(t *testing.T) { + t1 := time.Now() + t2 := t1.Add(time.Hour) + + type test struct { + options []RequestBodyOption + want *RequestBody + wantErr bool + } + tests := map[string]test{ + "Permanent Identifier": { + options: []RequestBodyOption{WithAttestationData(&AttestationData{PermanentIdentifier: "mydevice123"})}, + want: &RequestBody{ + AttestationData: &AttestationData{ + PermanentIdentifier: "mydevice123", + }, + }, + wantErr: false, + }, + "X509 Certificate Request": { + options: []RequestBodyOption{ + WithX509CertificateRequest(&x509.CertificateRequest{ + PublicKeyAlgorithm: x509.ECDSA, + Subject: pkix.Name{CommonName: "foo"}, + Raw: []byte("csr der"), + }), + }, + want: &RequestBody{ + X509CertificateRequest: &X509CertificateRequest{ + CertificateRequest: &x509util.CertificateRequest{ + PublicKeyAlgorithm: x509.ECDSA, + Subject: x509util.Subject{CommonName: "foo"}, + }, + PublicKeyAlgorithm: "ECDSA", + Raw: []byte("csr der"), + }, + }, + wantErr: false, + }, + "X509 Certificate": { + options: []RequestBodyOption{ + WithX509Certificate(&x509util.Certificate{}, &x509.Certificate{ + NotBefore: t1, + NotAfter: t2, + PublicKeyAlgorithm: x509.ECDSA, + }), + }, + want: &RequestBody{ + X509Certificate: &X509Certificate{ + Certificate: &x509util.Certificate{}, + PublicKeyAlgorithm: "ECDSA", + NotBefore: t1, + NotAfter: t2, + }, + }, + }, + "SSH Certificate Request": { + options: []RequestBodyOption{ + WithSSHCertificateRequest(sshutil.CertificateRequest{ + Type: "User", + KeyID: "key1", + Principals: []string{"areed", "other"}, + })}, + want: &RequestBody{ + SSHCertificateRequest: &SSHCertificateRequest{ + Type: "User", + KeyID: "key1", + Principals: []string{"areed", "other"}, + }, + }, + wantErr: false, + }, + "SSH Certificate": { + options: []RequestBodyOption{ + WithSSHCertificate( + &sshutil.Certificate{}, + &ssh.Certificate{ + ValidAfter: uint64(t1.Unix()), + ValidBefore: uint64(t2.Unix()), + }, + ), + }, + want: &RequestBody{ + SSHCertificate: &SSHCertificate{ + Certificate: &sshutil.Certificate{}, + ValidAfter: uint64(t1.Unix()), + ValidBefore: uint64(t2.Unix()), + }, + }, + wantErr: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := NewRequestBody(test.options...) + if (err != nil) != test.wantErr { + t.Fatalf("Got err %v, wanted %t", err, test.wantErr) + } + assert.Equals(t, test.want, got) + }) + } +} diff --git a/webhook/types.go b/webhook/types.go new file mode 100644 index 00000000..19624f5c --- /dev/null +++ b/webhook/types.go @@ -0,0 +1,71 @@ +package webhook + +import ( + "time" + + "go.step.sm/crypto/sshutil" + "go.step.sm/crypto/x509util" +) + +// ResponseBody is the body returned by webhook servers. +type ResponseBody struct { + Data any `json:"data"` + Allow bool `json:"allow"` +} + +// X509CertificateRequest is the certificate request sent to webhook servers for +// enriching webhooks when signing x509 certificates +type X509CertificateRequest struct { + *x509util.CertificateRequest + PublicKey []byte `json:"publicKey"` + PublicKeyAlgorithm string `json:"publicKeyAlgorithm"` + Raw []byte `json:"raw"` +} + +// X509Certificate is the certificate sent to webhook servers for authorizing +// webhooks when signing x509 certificates +type X509Certificate struct { + *x509util.Certificate + PublicKey []byte `json:"publicKey"` + PublicKeyAlgorithm string `json:"publicKeyAlgorithm"` + NotBefore time.Time `json:"notBefore"` + NotAfter time.Time `json:"notAfter"` +} + +// SSHCertificateRequest is the certificate request sent to webhook servers for +// enriching webhooks when signing SSH certificates +type SSHCertificateRequest struct { + PublicKey []byte `json:"publicKey"` + Type string `json:"type"` + KeyID string `json:"keyID"` + Principals []string `json:"principals"` +} + +// SSHCertificate is the certificate sent to webhook servers for authorizing +// webhooks when signing SSH certificates +type SSHCertificate struct { + *sshutil.Certificate + PublicKey []byte `json:"publicKey"` + SignatureKey []byte `json:"signatureKey"` + ValidBefore uint64 `json:"validBefore"` + ValidAfter uint64 `json:"validAfter"` +} + +// AttestationData is data validated by acme device-attest-01 challenge +type AttestationData struct { + PermanentIdentifier string `json:"permanentIdentifier"` +} + +// RequestBody is the body sent to webhook servers. +type RequestBody struct { + Timestamp time.Time `json:"timestamp"` + // Only set after successfully completing acme device-attest-01 challenge + AttestationData *AttestationData `json:"attestationData,omitempty"` + // Set for most provisioners, but not acme or scep + // Token any `json:"token,omitempty"` + // Exactly one of the remaining fields should be set + X509CertificateRequest *X509CertificateRequest `json:"x509CertificateRequest,omitempty"` + X509Certificate *X509Certificate `json:"x509Certificate,omitempty"` + SSHCertificateRequest *SSHCertificateRequest `json:"sshCertificateRequest,omitempty"` + SSHCertificate *SSHCertificate `json:"sshCertificate,omitempty"` +}