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/.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/CHANGELOG.md b/CHANGELOG.md index d2f50379..5ac4209b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ 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. ## [0.22.1] - 2022-08-31 ### Fixed 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 7d527b13..5271842d 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 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/options.go b/authority/options.go index 8e1a01ff..f332d4a9 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,14 @@ func WithDatabase(d db.AuthDB) Option { } } +// 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..d68c0b93 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,6 +253,7 @@ 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 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/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/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..7cb8593f 100644 --- a/authority/provisioner/x5c_test.go +++ b/authority/provisioner/x5c_test.go @@ -468,7 +468,7 @@ func TestX5C_AuthorizeSign(t *testing.T) { } 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 +493,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)) } @@ -794,15 +796,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 72485ddb..33694cf9 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -144,6 +144,7 @@ func (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner. GetIdentityFunc: a.getIdentityFunc, AuthorizeRenewFunc: a.authorizeRenewFunc, AuthorizeSSHRenewFunc: a.authorizeSSHRenewFunc, + WebhookClient: a.webhookClient, }, nil } @@ -493,9 +494,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 +676,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 +695,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 +711,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 { @@ -676,6 +736,17 @@ func provisionerPEMToLinkedca(b []byte) [][]byte { return roots } +func provisionerPEMToCertificates(bs [][]byte) []byte { + var roots []byte + for i, root := range bs { + if i > 0 && !bytes.HasSuffix(root, []byte{'\n'}) { + roots = append(roots, '\n') + } + roots = append(roots, root...) + } + return roots +} + // ProvisionerToCertificates converts the linkedca provisioner type to the certificates provisioner // interface. func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, error) { @@ -755,6 +826,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, RequireEAB: cfg.RequireEab, Challenges: challengesToCertificates(cfg.Challenges), AttestationFormats: attestationFormatsToCertificates(cfg.AttestationFormats), + AttestationRoots: provisionerPEMToCertificates(cfg.AttestationRoots), Claims: claims, Options: options, }, nil @@ -867,7 +939,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 } @@ -890,9 +962,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 } @@ -917,9 +990,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 } @@ -941,9 +1015,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 } @@ -964,9 +1039,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 } @@ -990,9 +1066,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 } @@ -1006,15 +1083,17 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro ForceCn: p.ForceCN, Challenges: challengesToLinkedca(p.Challenges), AttestationFormats: attestationFormatsToLinkedca(p.AttestationFormats), + AttestationRoots: provisionerPEMToLinkedca(p.AttestationRoots), }, }, }, 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 } @@ -1032,9 +1111,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 } @@ -1052,6 +1132,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{ @@ -1066,7 +1147,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 } @@ -1089,9 +1170,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 } @@ -1109,6 +1191,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..ad214030 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) { @@ -251,3 +252,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..c39b9901 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) { diff --git a/authority/tls.go b/authority/tls.go index efabc8f2..6ec98088 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -28,6 +28,7 @@ import ( casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/webhook" ) // GetTLSOptions returns the tls options configured. @@ -93,7 +94,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 @@ -131,14 +133,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 @@ -223,6 +236,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{ @@ -699,3 +720,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 bc9e3e3a..7b5a0b6c 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -547,6 +547,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) @@ -634,6 +664,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{} 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..ab2aa8ac 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -156,17 +156,22 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { opts = append(opts, authority.WithDatabase(ca.opts.database)) } + 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 +225,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), + ) }) } } @@ -456,13 +467,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 +484,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 +504,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 +531,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 diff --git a/go.mod b/go.mod index bf8eb094..5b4e432b 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/smallstep/certificates go 1.18 require ( - cloud.google.com/go v0.100.2 - cloud.google.com/go/security v1.3.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 @@ -20,44 +20,44 @@ 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.5.1 + github.com/hashicorp/vault/api v1.8.0 + 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/rs/xid v1.4.0 github.com/sirupsen/logrus v1.8.1 - github.com/slackhq/nebula v1.5.2 + 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.7.1 - github.com/urfave/cli v1.22.4 + github.com/stretchr/testify v1.8.0 + 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.1 + go.step.sm/cli-utils v0.7.5 + go.step.sm/crypto v0.19.1-0.20220929182301-ae99d3fe3185 + 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-20220920203100-d0c6ba3f52d9 - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/net v0.0.0-20220927171203-f486391704dc + golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect - google.golang.org/api v0.84.0 - google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad - google.golang.org/grpc v1.47.0 - google.golang.org/protobuf v1.28.0 + google.golang.org/api v0.98.0 + google.golang.org/genproto v0.0.0-20220929141241-1ce7b20da813 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 gopkg.in/square/go-jose.v2 v2.6.0 ) require ( - cloud.google.com/go/compute v1.6.1 // indirect - cloud.google.com/go/iam v0.1.0 // indirect + cloud.google.com/go/compute v1.7.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 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect @@ -82,12 +82,12 @@ 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/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.0.0-20220520183353-fd19c99a87aa // indirect + github.com/googleapis/enterprise-certificate-proxy v0.1.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,14 +97,14 @@ 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 @@ -124,7 +124,7 @@ require ( 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,14 +139,16 @@ 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-20220608161450-d0670ef3b1eb // indirect - golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect + golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect + golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect google.golang.org/appengine v1.6.7 // indirect - gopkg.in/yaml.v3 v3.0.0 // 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 diff --git a/go.sum b/go.sum index 90969016..59e98e5e 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,10 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= 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 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= 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.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,25 +41,28 @@ cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTB 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 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= 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/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 h1:W2vbGCrE3Z7J/x3WXLxxGl9LMSB2uhsAA7Ss/6u/qRY= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/security v1.3.0 h1:BhCl33x+KQI4qiZnFrfr2gAGhb2aZ0ZvKB3Y4QlEfgo= -cloud.google.com/go/security v1.3.0/go.mod h1:pQsnLAXfMzuWVJdctBs8BV3tGd3Jr0SMYu6KK3QXYAs= +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= @@ -68,8 +73,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= @@ -245,8 +250,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= @@ -321,8 +327,9 @@ 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= @@ -348,16 +355,19 @@ 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 h1:7MYGT2XEMam7Mtzv1yDUYXANedWvwk3HKkR3MyGowy8= 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/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/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= 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= @@ -387,7 +397,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= @@ -403,11 +413,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= @@ -430,15 +442,14 @@ 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 h1:7765sW1XBt+qf4XKIYE4ebY9qc/yi9V2/egzGSUNMZU= +github.com/hashicorp/vault/api v1.8.0/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= @@ -589,8 +600,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= @@ -606,8 +617,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= @@ -669,8 +680,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= @@ -693,8 +705,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.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/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= @@ -725,16 +737,18 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 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/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= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 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/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= @@ -743,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= @@ -771,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.1 h1:8XcQvanelK1g0ijl5/itmmAIsqD2QSMHGqcWzJwwJCU= -go.step.sm/linkedca v0.19.0-rc.1/go.mod h1:G35baT7Qnh6VsRCjzSfi5xsYw0ERrU+I1aIuZswMBeA= +go.step.sm/crypto v0.19.1-0.20220929182301-ae99d3fe3185 h1:W+UhojTrFZngWTudpP3n9vPs4UNLudVSkKrWZuZg/RU= +go.step.sm/crypto v0.19.1-0.20220929182301-ae99d3fe3185/go.mod h1:972LarNeN9dgx4+zkF3fHCnTWLXzuQSIOdMaGeIslUY= +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.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= @@ -809,6 +823,7 @@ 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-20220722155217-630584e8d5aa/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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -898,8 +913,10 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su 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-20220920203100-d0c6ba3f52d9 h1:asZqf0wXastQr+DudYagQS8uBO8bHKeYD1vbAvGmFL8= -golang.org/x/net v0.0.0-20220920203100-d0c6ba3f52d9/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +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/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= @@ -919,8 +936,10 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ 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 h1:8tDJ3aechhddbdPAxpycgXHJRMLpk/Ab+aa4OgdN5/g= 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/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= @@ -1009,10 +1028,12 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc 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 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= 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/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= @@ -1026,8 +1047,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-0.20211105212822-18b340fc7af2 h1:GLw7MR8AfAG2GmGcmVgObFOHXYypgGjnGno25RDwn3Y= +golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= 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= @@ -1103,6 +1124,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T 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= @@ -1142,8 +1164,10 @@ google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc 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.84.0 h1:NMB9J4cCxs9xEm+1Z9QiO3eFvn7EnQj3Eo3hN6ugVlg= +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.98.0 h1:yxZrcxXESimy6r6mdL5Q6EnZwmewDJK2dVg3g75s5Dg= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= 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= @@ -1194,6 +1218,7 @@ 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= @@ -1228,9 +1253,13 @@ google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX 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-20220617124728-180714bec0ad h1:kqrS+lhvaMHCxul6sKQvKJ8nAAhlVItmZV822hYFH/U= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/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-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/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= @@ -1267,8 +1296,10 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI 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.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +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/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= @@ -1284,13 +1315,15 @@ 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 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 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= @@ -1313,8 +1346,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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"` +}