diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml index a25de591ba..58da4b9ddd 100644 --- a/.github/workflows/depsreview.yaml +++ b/.github/workflows/depsreview.yaml @@ -1,14 +1,15 @@ name: 'Dependency Review' on: [pull_request] -permissions: - contents: read - jobs: dependency-review: runs-on: ubuntu-latest + + permissions: + contents: read + steps: - name: 'Checkout Repository' uses: actions/checkout@v3 - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 + uses: actions/dependency-review-action@v3 diff --git a/.github/workflows/nightly_build.yaml b/.github/workflows/nightly_build.yaml index f276bea2e8..7eddbd6b6d 100644 --- a/.github/workflows/nightly_build.yaml +++ b/.github/workflows/nightly_build.yaml @@ -4,13 +4,18 @@ on: # Random minute number to avoid GH scheduler stampede - cron: '37 21 * * *' workflow_dispatch: {} -permissions: - contents: read - packages: write + +env: + NIGHTLY: true jobs: build-and-publish-images: runs-on: ubuntu-20.04 + + permissions: + contents: read + packages: write + steps: - name: Checkout uses: actions/checkout@v3 @@ -31,4 +36,4 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push images - run: ./.github/workflows/scripts/push-scratch-images.sh nightly + run: ./.github/workflows/scripts/push-images.sh nightly -scratch diff --git a/.github/workflows/pr_build.yaml b/.github/workflows/pr_build.yaml index b86afd9616..4bed964df6 100644 --- a/.github/workflows/pr_build.yaml +++ b/.github/workflows/pr_build.yaml @@ -3,7 +3,7 @@ on: pull_request: {} workflow_dispatch: {} env: - GO_VERSION: 1.19.2 + GO_VERSION: 1.19.4 permissions: contents: read @@ -11,6 +11,10 @@ jobs: cache-deps: name: cache-deps (linux) runs-on: ubuntu-20.04 + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -30,6 +34,10 @@ jobs: name: lint (linux) runs-on: ubuntu-20.04 needs: cache-deps + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -64,6 +72,10 @@ jobs: OS: [ubuntu-20.04, macos-latest] runs-on: ${{ matrix.OS }} needs: cache-deps + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -83,6 +95,10 @@ jobs: name: unit-test (linux with race detection) runs-on: ubuntu-20.04 needs: cache-deps + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -102,6 +118,10 @@ jobs: name: artifacts (linux) runs-on: ubuntu-20.04 needs: [cache-deps] + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -133,6 +153,10 @@ jobs: name: images (linux) runs-on: ubuntu-20.04 needs: [cache-deps] + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -166,6 +190,10 @@ jobs: name: images (windows) runs-on: windows-2022 needs: artifact-windows + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -189,6 +217,10 @@ jobs: scratch-images: runs-on: ubuntu-20.04 needs: [cache-deps] + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -222,6 +254,10 @@ jobs: name: integration (linux) runs-on: ubuntu-20.04 needs: [cache-deps, images, scratch-images] + + permissions: + contents: read + strategy: fail-fast: false matrix: @@ -278,8 +314,10 @@ jobs: name: integration (windows) runs-on: windows-2022 needs: images-windows - strategy: - fail-fast: false + + permissions: + contents: read + defaults: run: shell: msys2 {0} @@ -327,6 +365,10 @@ jobs: cache-deps-windows: name: cache-deps (windows) runs-on: windows-2022 + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -346,6 +388,10 @@ jobs: name: lint (windows) runs-on: windows-2022 needs: cache-deps-windows + + permissions: + contents: read + defaults: run: shell: msys2 {0} @@ -379,7 +425,7 @@ jobs: mingw-w64-x86_64-toolchain unzip - name: Lint - run: make lint + run: make lint-code - name: Tidy check run: make tidy-check - name: Generate check @@ -389,6 +435,10 @@ jobs: name: unit-test (windows) runs-on: windows-2022 needs: cache-deps-windows + + permissions: + contents: read + defaults: run: shell: msys2 {0} @@ -421,6 +471,10 @@ jobs: name: artifact (windows) runs-on: windows-2022 needs: cache-deps-windows + + permissions: + contents: read + defaults: run: shell: msys2 {0} diff --git a/.github/workflows/release_build.yaml b/.github/workflows/release_build.yaml index 820ddde217..447c8e7bce 100644 --- a/.github/workflows/release_build.yaml +++ b/.github/workflows/release_build.yaml @@ -4,11 +4,15 @@ on: tags: - 'v[0-9].[0-9]+.[0-9]+' env: - GO_VERSION: 1.19.2 + GO_VERSION: 1.19.4 jobs: cache-deps: name: cache-deps (linux) runs-on: ubuntu-20.04 + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -28,6 +32,10 @@ jobs: name: lint (linux) runs-on: ubuntu-20.04 needs: cache-deps + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -62,6 +70,10 @@ jobs: OS: [ubuntu-20.04, macos-latest] runs-on: ${{ matrix.OS }} needs: cache-deps + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -81,6 +93,10 @@ jobs: name: unit-test (linux with race detection) runs-on: ubuntu-20.04 needs: cache-deps + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -100,6 +116,10 @@ jobs: name: artifacts (linux) runs-on: ubuntu-20.04 needs: [cache-deps] + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -131,6 +151,10 @@ jobs: name: images (linux) runs-on: ubuntu-20.04 needs: [cache-deps] + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -164,6 +188,10 @@ jobs: name: images (windows) runs-on: windows-2022 needs: artifact-windows + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -187,6 +215,10 @@ jobs: scratch-images: runs-on: ubuntu-20.04 needs: [cache-deps] + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -220,6 +252,10 @@ jobs: name: integration (linux) runs-on: ubuntu-20.04 needs: [cache-deps, images, scratch-images] + + permissions: + contents: read + strategy: fail-fast: false matrix: @@ -287,8 +323,10 @@ jobs: name: integration (windows) runs-on: windows-2022 needs: images-windows - strategy: - fail-fast: false + + permissions: + contents: read + defaults: run: shell: msys2 {0} @@ -336,6 +374,10 @@ jobs: cache-deps-windows: name: cache-deps (windows) runs-on: windows-2022 + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -355,6 +397,10 @@ jobs: name: lint (windows) runs-on: windows-2022 needs: cache-deps-windows + + permissions: + contents: read + defaults: run: shell: msys2 {0} @@ -388,7 +434,7 @@ jobs: mingw-w64-x86_64-toolchain unzip - name: Lint - run: make lint + run: make lint-code - name: Tidy check run: make tidy-check - name: Generate check @@ -398,6 +444,10 @@ jobs: name: unit-test (windows) runs-on: windows-2022 needs: cache-deps-windows + + permissions: + contents: read + defaults: run: shell: msys2 {0} @@ -430,6 +480,10 @@ jobs: name: artifact (windows) runs-on: windows-2022 needs: cache-deps-windows + + permissions: + contents: read + defaults: run: shell: msys2 {0} @@ -480,6 +534,10 @@ jobs: runs-on: ubuntu-20.04 needs: [lint, unit-test, unit-test-race-detector, artifacts, integration, lint-windows, unit-test-windows, artifact-windows, integration-windows] + + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v3 @@ -503,6 +561,11 @@ jobs: publish-images: runs-on: ubuntu-20.04 needs: [lint, unit-test, unit-test-race-detector, artifacts, integration] + + permissions: + contents: read + packages: write + steps: - name: Checkout uses: actions/checkout@v3 @@ -526,15 +589,13 @@ jobs: registry: gcr.io username: _json_key password: ${{ secrets.GCR_JSON_KEY }} - # Push the images to GCR using the version number (without the "v" prefix). - name: Push images - run: ./.github/workflows/scripts/push-images.sh "${GITHUB_REF#refs/tags/v}" + run: ./.github/workflows/scripts/push-images.sh "${GITHUB_REF}" - name: Log in to GHCR uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Push the images to GHCR using the version number (without the "v" prefix). - name: Push images - run: ./.github/workflows/scripts/push-scratch-images.sh "${GITHUB_REF#refs/tags/v}" + run: ./.github/workflows/scripts/push-images.sh "${GITHUB_REF}" -scratch diff --git a/.github/workflows/scripts/push-images.sh b/.github/workflows/scripts/push-images.sh index 60e859c63b..4e9ba19150 100755 --- a/.github/workflows/scripts/push-images.sh +++ b/.github/workflows/scripts/push-images.sh @@ -1,20 +1,66 @@ -#!/bin/bash +#!/usr/bin/env bash +# shellcheck shell=bash +## +## USAGE: __PROG__ +## +## "__PROG__" publishes images to a registry. +## +## Usage example(s): +## ./__PROG__ 1.5.2 +## ./__PROG__ v1.5.2 +## ./__PROG__ v1.5.2 -scratch +## ./__PROG__ refs/tags/v1.5.2 +## ./__PROG__ refs/tags/v1.5.2 -scratch +## +## Commands +## - ./__PROG__ [image-variant] pushes images to the registry using given version. set -e -IMAGETAG="$1" -if [ -z "$IMAGETAG" ]; then - echo "IMAGETAG not provided!" 1>&2 - echo "Usage: push-images.sh IMAGETAG" 1>&2 - exit 1 +function usage { + grep '^##' "$0" | sed -e 's/^##//' -e "s/__PROG__/$me/" >&2 +} + +me=$(basename "$0") + +version="$1" +if [ -z "${version}" ]; then + usage + echo -e "\n Errors:\n * the version must be provided." >&2 + exit 1 fi -echo "Pushing images tagged as $IMAGETAG..." +# remove the git tag prefix +# Push the images using the version tag (without the "v" prefix). +# Also strips the refs/tags part if the GITHUB_REF variable is used. +version="${version#refs/tags/v}" +version="${version#v}" + +variant="$2" +if [ -n "${variant}" ] && [ "${variant}" != "-scratch" ]; then + usage + echo -e "\n Errors:\n * The only supported variant is '-scratch'." >&2 + exit 1 +fi + +OCI_IMAGES=( + spire-server spire-agent oidc-discovery-provider +) + +registry=gcr.io/spiffe-io +if [ "${variant}" = "-scratch" ] ; then + org_name=$(echo "$GITHUB_REPOSITORY" | tr '/' "\n" | head -1 | tr -d "\n") + org_name="${org_name:-spiffe}" # default to spiffe in case ran on local + registry=ghcr.io/${org_name} +else + # Continue publishing the non-scratch k8s-workload-registrar to GCR + OCI_IMAGES+=( k8s-workload-registrar ) +fi -for img in spire-server spire-agent k8s-workload-registrar oidc-discovery-provider; do - gcrimg=gcr.io/spiffe-io/"$img":"${IMAGETAG}" - echo "Executing: docker tag $img:latest-local $gcrimg" - docker tag "$img":latest-local "$gcrimg" - echo "Executing: docker push $gcrimg" - docker push "$gcrimg" +echo "Pushing images ${OCI_IMAGES[*]} to ${registry} with tag ${version}". +for img in "${OCI_IMAGES[@]}"; do + image_variant="${img}${variant}" + image_to_push="${registry}/${img}:${version}" + docker tag "${image_variant}:latest-local" "${image_to_push}" + docker push "${image_to_push}" done diff --git a/.github/workflows/scripts/push-scratch-images.sh b/.github/workflows/scripts/push-scratch-images.sh deleted file mode 100755 index a532740c6f..0000000000 --- a/.github/workflows/scripts/push-scratch-images.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -set -e - -IMAGETAG="$1" -if [ -z "$IMAGETAG" ]; then - echo "IMAGETAG not provided!" 1>&2 - echo "Usage: push-images.sh IMAGETAG" 1>&2 - exit 1 -fi - -# Extracting org name rather than hardcoding allows this -# action to be portable across forks -ORGNAME=$(echo "$GITHUB_REPOSITORY" | tr '/' "\n" | head -1 | tr -d "\n") - -echo "Pushing images tagged as $IMAGETAG..." - -for img in spire-server spire-agent oidc-discovery-provider; do - ghcrimg="ghcr.io/${ORGNAME}/${img}:${IMAGETAG}" - - # Detect the oidc image and give it a different name for GHCR - # TODO: Remove this hack and fully rename the image once we move - # off of GCR. - if [ "$img" == "oidc-discovery-provider" ]; then - ghcrimg="ghcr.io/${ORGNAME}/spire-oidc-provider:${IMAGETAG}" - fi - - echo "Executing: docker tag $img-scratch:latest-local $ghcrimg" - docker tag "$img"-scratch:latest-local "$ghcrimg" - echo "Executing: docker push $ghcrimg" - docker push "$ghcrimg" -done diff --git a/.github/workflows/scripts/run_unit_tests.sh b/.github/workflows/scripts/run_unit_tests.sh index 558f5f970d..501af923ed 100755 --- a/.github/workflows/scripts/run_unit_tests.sh +++ b/.github/workflows/scripts/run_unit_tests.sh @@ -14,3 +14,7 @@ if [ -n "${COVERALLS_TOKEN}" ]; then "$(go env GOPATH)"/bin/goveralls -coverprofile="${COVERPROFILE}" \ -service=github fi + +# This ensures that running the tests didn't modify the source files, for +# example by generating test keys that should have been checked in with the PR. +make git-clean-check diff --git a/.github/workflows/scripts/run_unit_tests_under_race_detector.sh b/.github/workflows/scripts/run_unit_tests_under_race_detector.sh index 6a64da56cb..4581c6b501 100755 --- a/.github/workflows/scripts/run_unit_tests_under_race_detector.sh +++ b/.github/workflows/scripts/run_unit_tests_under_race_detector.sh @@ -8,9 +8,13 @@ if [ -n "${COVERALLS_TOKEN}" ]; then go install github.com/mattn/goveralls@v0.0.7 fi -COVERPROFILE="${COVERPROFILE}" make ci-race-test +COVERPROFILE="${COVERPROFILE}" make race-test if [ -n "${COVERALLS_TOKEN}" ]; then "$(go env GOPATH)"/bin/goveralls -coverprofile="${COVERPROFILE}" \ -service=github fi + +# This ensures that running the tests didn't modify the source files, for +# example by generating test keys that should have been checked in with the PR. +make git-clean-check diff --git a/.gitignore b/.gitignore index aa533582c2..449485a6a9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .data .glide .tmp +.DS_Store *.swp *.log /bin @@ -29,3 +30,7 @@ tools/spire-plugingen/spire-plugingen # Editor specific configuration .idea .vscode + +# Runtime version manager specific configuration +# asdf config file +.tool-versions diff --git a/.go-version b/.go-version index 836ae4eda2..843f863534 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.19.2 +1.19.4 diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000000..b6f65f9276 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,7 @@ +MD013: false +# We are not interested on requesting output when using "$" on shell documentation +MD014: false +MD024: + siblings_only: true +# we use emphasis on all node attestors +MD036: false diff --git a/ADOPTERS.md b/ADOPTERS.md index 05e192f39e..4f449106b1 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,34 +1,37 @@ +# Adopters + ## End users -Known end users with notable contributions to the advancement of the project include: +Known end users with notable contributions to the advancement of the project include: * Anthem -* Bloomberg -* ByteDance +* Bloomberg +* ByteDance * Duke Energy * GitHub * Netflix * Niantic -* Pinterest +* Pinterest * Square -* Twilio +* Twilio * Uber +* Unity Technologies * Z Lab Corporation -SPIFFE and SPIRE are being used by numerous other companies, both large and small, to build higher layer products and services. The list includes but is not limited to: +SPIFFE and SPIRE are being used by numerous other companies, both large and small, to build higher layer products and services. The list includes but is not limited to: * Amazon * Arm -* Cisco -* Decipher Technology Studios -* F5 Networks -* HashiCorp +* Cisco +* Decipher Technology Studios +* F5 Networks +* HashiCorp * Hewlett Packard Enterprise -* Intel -* Google -* IBM +* Intel +* Google +* IBM * SAP -* Tigera +* Tigera * TestifySec * Transferwise * VMware @@ -37,70 +40,64 @@ SPIFFE and SPIRE are being used by numerous other companies, both large and smal SPIFFE and SPIRE have integrations available with a number of open-source projects. The list includes but is not limited to: -* [App Mesh Controller](https://github.com/aws/aws-app-mesh-controller-for-k8s) +* [App Mesh Controller](https://github.com/aws/aws-app-mesh-controller-for-k8s) * [Athenz](https://github.com/yahoo/athenz) -* [Cert-Manager](https://github.com/cert-manager/csi-driver-spiffe) -* [Consul](https://github.com/hashicorp/consul) -* [Dapr](https://github.com/dapr) -* [Docker](https://github.com/containerd/containerd) -* [Emissary](https://github.com/github/emissary) -* [Envoy](https://github.com/envoyproxy/envoy) -* [Ghostunnel](https://github.com/square/ghostunnel) -* [gRPC](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/examples/spiffe-grpc) -* [Hamlet](https://github.com/vmware/hamlet) -* [Istio](https://github.com/istio/istio) -* [Knox](https://github.com/pinterest/knox) -* [Kubernetes](https://github.com/kubernetes/kubernetes) -* [NGINX](http://hg.nginx.org/nginx/) -* [Parsec](https://github.com/parallaxsecond/parsec) -* [Sigstore](https://github.com/sigstore/fulcio) -* [Tekton](https://github.com/tektoncd/chains) -* [Tornjak](https://github.com/spiffe/tornjak) - +* [Cert-Manager](https://github.com/cert-manager/csi-driver-spiffe) +* [Consul](https://github.com/hashicorp/consul) +* [Dapr](https://github.com/dapr) +* [Docker](https://github.com/containerd/containerd) +* [Emissary](https://github.com/github/emissary) +* [Envoy](https://github.com/envoyproxy/envoy) +* [Ghostunnel](https://github.com/square/ghostunnel) +* [gRPC](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/examples/spiffe-grpc) +* [Hamlet](https://github.com/vmware/hamlet) +* [Istio](https://github.com/istio/istio) +* [Knox](https://github.com/pinterest/knox) +* [Kubernetes](https://github.com/kubernetes/kubernetes) +* [NGINX](http://hg.nginx.org/nginx/) +* [Parsec](https://github.com/parallaxsecond/parsec) +* [Sigstore](https://github.com/sigstore/fulcio) +* [Tekton](https://github.com/tektoncd/chains) +* [Tornjak](https://github.com/spiffe/tornjak) ## Case Studies/User Stories * Amazon Web Services blogs about using mTLS with SPIFFE/SPIRE in AWS App Mesh on Amazon EKS -https://aws.amazon.com/blogs/containers/using-mtls-with-spiffe-spire-in-app-mesh-on-eks/ + * Anthem writes about developing a zero trust framework at Anthem Using SPIFFE and SPIRE: -https://upshotstories.com/stories/developing-a-zero-trust-framework-at-anthem-using-spiffe-and-spire + -* ARM and VMware showcase hardware backed security for multitenancy at the Edge with SPIFFE & PARSEC -https://www.youtube.com/watch?v=-I_rCKMyY7Y +* ARM and VMware showcase hardware backed security for multi-tenancy at the Edge with SPIFFE & PARSEC + * Bloomberg talks about TPM node attestation with SPIRE: -https://youtu.be/30S0sKRxzjM + * Coinbase details Container Technologies part of their stack: -https://blog.coinbase.com/container-technologies-at-coinbase-d4ae118dcb6c + -* Duke Energy describes securing the Microgrid using SPIFFE and SPIRE with TPMs -https://www.distributech.com/distributech-international-2022-conference-sessions/achieving-the-promise-of-grid-security-with-openfmb-and-cybersecurity-zero-trust-best-practices +* Duke Energy describes securing the Microgrid using SPIFFE and SPIRE with TPMs + * NGINX/F5 on how NGINX service mesh leverages SPIFFE and SPIRE -https://youtu.be/plRkDK5xFpM + * Styra demonstrates fortifying microservices with SPIRE and OPA -https://www.youtube.com/watch?v=iQ5ctLQswUc + * Square talks about how Square uses SPIFFE and SPIRE to secure communications across hybrid infrastructure services: -https://youtu.be/H5IlmYmEDKk?t=2585 + * Square describes how they provide mTLS identities to Lambdas using SPIFFE and SPIRE -https://developer.squareup.com/blog/providing-mtls-identities-to-lambdas/ + * Tigera demonstrates how Calico, Envoy and SPIRE are used to deliver unified Layer 4 and Layer 7 authorization policies: -https://youtu.be/H5IlmYmEDKk?t=7812 - -* Uber talks about integrating SPIRE with workload schedulers: -https://youtu.be/H5IlmYmEDKk?t=4703 - - + +* Uber talks about integrating SPIRE with workload schedulers: + ## Adding a name -If you would like to add your name to this file, submit a pull request with your change. - - +If you would like to add your name to this file, submit a pull request with your change. diff --git a/CHANGELOG.md b/CHANGELOG.md index c4927f865d..1dd0e7b993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,39 +1,129 @@ # Changelog +## [1.5.3] - 2022-12-14 + +### Added + +- A new `gcp_kms` KeyManager plugin is now available (#3410, #3638, #3653, #3655) +- `spire-server agent`, `spire-server bundle`, and `spire-server entry` CLI commands now support `-output` flag (#3523, #3624, #3628) + +### Changed + +- SPIRE-managed files on Windows no longer inherit permissions from parent directory (#3577, #3604) +- Documentation improvements (#3534, #3546, #3461, #3565, #3630, #3632, #3639,) + +### Fixed + +- oidc-discovery-provider healthcheck HTTP server now binds to all network interfaces for visibility outside containers using virtual IP (#3580) +- k8s-workload-registrar CRD and reconcile modes now have correct example leader election RBAC YAML (#3617) + +## [1.5.2] - 2022-12-06 + +### Security + +- Updated to Go 1.19.4 to address CVE-2022-41717. + +## [1.5.1] - 2022-11-08 + +### Fixed + +- The deprecated `default_svid_ttl` configurable is now correctly observed after fixing a regression introduced in 1.5.0 (#3583) + +## [1.5.0] - 2022-11-02 + +### Added + +- X.509-SVID and JWT-SVID TTLs can now be configured separately at both the entry-level and Server default level (#3445) +- Entry protobuf type in `/v1/entry` API includes new `jwt_svid_ttl` field (#3445) +- `k8s-workload-registrar` and `oidc-discovery-provider` CLIs now print their version when the `-version` flag is set (#3475) +- Support for customizing SPIFFE ID paths of SPIRE Agents attested with the `azure_msi` NodeAttestor plugin (#3488) + +### Changed + +- Entry `ttl` protobuf field in `/v1/entry` API is renamed to `x509_ttl` (#3445) +- External plugins can no longer be named `join_token` to avoid conflicts with the builtin plugin (#3469) +- `spire-server run` command now supports DNS names for the configured bind address (#3421) +- Documentation improvements (#3468, #3472, #3473, #3474, #3515) + +### Deprecated + +- `k8s-workload-registrar` is deprecated in favor of [SPIRE Controller Manager](https://github.com/spiffe/spire-controller-manager) (#3526) +- Server `default_svid_ttl` configuration field is deprecated in favor of `default_x509_svid_ttl` and `default_jwt_svid_ttl` fields (#3445) +- `-ttl` flag in `spire-server entry create` and `spire-server entry update` commands is deprecated in favor of `-x509SVIDTTL` and `-jwtSVIDTTL` flags (#3445) +- `-format` flag in `spire-agent fetch jwt` CLI command is deprecated in favor of `-output` flag (#3528) +- `InMem` telemetry collector is deprecated and no longer enabled by default (#3492) + +### Removed + +- NodeResolver plugin type and `azure_msi` builtin NodeResolver plugin (#3470) + +## [1.4.6] - 2022-12-06 + +### Security + +- Updated to Go 1.19.4 to address CVE-2022-41717. + +## [1.4.5] - 2022-11-01 + +### Security + +- Updated to Go 1.19.3 to address CVE-2022-41716. This vulnerability only affects users configuring external Server or Agent plugins on Windows. + +## [1.4.4] - 2022-10-05 + +### Added + +- Experimental support for limiting the number of SVIDs in the agent's cache (#3181) +- Support for attesting Envoy proxy workloads when Istio is configured with holdApplicationUntilProxyStarts (#3460) + +### Changed + +- Improved bundle endpoint misconfiguration diagnostics (#3395) +- OIDC Discovery Provider endpoint now has a timeout to read request headers (#3435) +- Small documentation improvements (#3443) + ## [1.4.3] - 2022-10-04 ### Security + - Updated minimum TLS version to 1.2 for the k8s-workload-registrar CRD mode webhook and the oidc-discovery-provider when using ACME ## [1.4.2] - 2022-09-07 ### Added + - The X509-SVID Subject field now contains a unique ID to satisfy RFC 5280 requirements (#3367) - Agents now shut down when banned (#3308) ### Changed + - Small documentation improvements (#3309, #3377) ## [1.4.1] - 2022-09-06 ### Security + - Updated to Go 1.18.6 to address CVE-2022-27664 ## [1.4.0] - 2022-08-08 -### Added +### Added + - Support for Windows workload attestation on Kubernetes (#3191) - Support for using RSA keys with Workload X509-SVIDs (#3237) - Support for anonymous authentication to the Kubelet secure port when performing workload attestation on Kubernetes (#3273) ### Deprecated + - The Node Resolver plugin type (#3272) ### Fixed + - Persistence of the can_reattest flag during agent SVID renewal (#3292) - A regression in behavior preventing an agent from re-attesting when it has been evicted (#3269) ### Changed + - The Azure Node Attestor to optionally provide selectors (#3272) - The Docker Workload Attestor now fails when configured with unknown options (#3243) - Improved CRI-O support with Kubernetes workload attestation (#3242) @@ -43,49 +133,65 @@ - Small documentation improvements (#3264) ### Removed + - The deprecated webhook mode from the k8s-workload-registrar (#3235) - Support for the configmap leader election lock type from the k8s-workload-registrar (#3241) +## [1.3.6] - 2022-11-01 + +### Security + +- Updated to Go 1.18.8 to address CVE-2022-41716. This vulnerability only affects users configuring external Server or Agent plugins on Windows. + ## [1.3.5] - 2022-10-04 ### Security + - Updated minimum TLS version to 1.2 for the k8s-workload-registrar CRD mode webhook and the oidc-discovery-provider when using ACME ## [1.3.4] - 2022-09-06 ### Security + - Updated to Go 1.18.6 to address CVE-2022-27664 ## [1.3.3] - 2022-07-13 ### Security + - Updated to Go 1.18.4 to address CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, CVE-2022-30633, CVE-2022-28131, CVE-2022-30635, CVE-2022-30632, CVE-2022-30630, and CVE-2022-1962. ## [1.3.2] - 2022-07-08 ### Added + - Support for K8s workload attestation when the Kubelet is run as a standalone component (#3163) - Optional health check endpoints to the OIDC Discovery Provider (#3151) - Pagination support to the server `entry show` command (#3135) ### Fixed + - A regression in workload SVID minting that caused DNS names not to be set in the SVID (#3215) - A regression in the server that caused a panic instead of a clean shutdown if a plugin was misconfigured (#3166) ### Changed + - Directories for UDS endpoints are no longer created by SPIRE on Windows (#3192) ## [1.3.1] - 2022-06-09 ### Added + - The `windows` workload attestor gained a new `sha256` selector that can attest the SHA256 digest of the workload binary (#3100) ### Fixed + - Database rows related to registration entries are now properly removed (#3127, #3132) - Agent reduces bandwidth use by requesting only required information when syncing with the server (#3123) - Issue with read-modify-write operations when using PostgreSQL datastore in hot standby mode (#3103) ### Changed + - FetchX509Bundles RPC no longer sends spurious updates that contain no changes (#3102) - Warn if the built-in `join_token` node attestor is attempted to be overridden by an external plugin (#3045) - Database connections are now proactively closed when SPIRE server is shut down (#3047) @@ -93,16 +199,19 @@ ## [1.3.0] - 2022-05-12 ### Added -- Experimental Windows support (https://github.com/spiffe/spire/projects/12) + +- Experimental Windows support () - Ability to revert SPIFFE cert validation to standard X.509 validation in Envoy (#3009, #3014, #3020, #3034) - Configurable leader election resource lock type for the K8s Workload Registrar (#3030) - Ability to fetch JWT SVIDs and JWT Bundles on behalf of workloads via the Delegated Identity API (#2789) - CanReattest flag to NodeAttestor responses to facilitate future features (#2646) ### Fixed + - Spurious message to STDOUT when there is no plugin_data section configured for a plugin (#2927) ### Changed + - SPIRE entries with malformed parent or SPIFFE IDs are removed on server startup (#2965) - SPIRE no longer prepends slashes to paths passed to the API when missing (#2963) - K8s Workload Registrar retries up to 5 seconds to connect to SPIRE Server (#2921) @@ -110,49 +219,59 @@ - Small documentation improvements (#2934, #2947, #3013) ### Deprecated + - The webhook mode for the K8s Workload Register has been deprecated (#2964) ## [1.2.5] - 2022-07-13 ### Security + - Updated to Go 1.17.12 to address CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, CVE-2022-30633, CVE-2022-28131, CVE-2022-30635, CVE-2022-30632, CVE-2022-30630, and CVE-2022-1962. ## [1.2.4] - 2022-05-12 ### Added + - Ability to revert SPIFFE cert validation to standard X.509 validation in Envoy (#3009,#3014,#3020,#3034) ## [1.2.3] - 2022-04-12 ### Security + - Updated to Go 1.17.9 to address CVE-2022-24675, CVE-2022-28327, CVE-2022-27536 ## [1.2.2] - 2022-04-07 ### Added + - SPIRE Server and Agent log files can be rotated by sending the `SIGUSR2` signal to the process (#2703) - K8s Workload Registrar CRD mode now supports registering "downstream" workloads (#2885) - SPIRE can now be compiled on macOS machines with an Apple Silicon CPU (#2876) - Small documentation improvements (#2851) ### Changed + - SPIRE Server no longer sets the `DigitalSignature` KeyUsage bit in its CA certificate (#2896) ### Fixed + - The `k8sbundle` Notifier plugin in SPIRE Server no longer consumes excessive CPU cycles (#2857) ## [1.2.1] - 2022-03-16 ### Added + - The SPIRE Agent `fetch jwt` CLI command now supports JSON output (#2650) ### Changed + - OIDC Discovery Provider now includes the `alg` parameter in JWKs to increase compatibility (#2771) - SPIRE Server now gracefully stops plugin servers, allowing outstanding RPCs a chance to complete (#2722) - SPIRE Server logs additional authorization information with RPC requests (#2776) - Small documentation improvements (#2746, #2792) ### Fixed + - SPIRE Server now properly rotates signing keys when prepared or activated keys are lost from the database (#2770) - The AWS IID node attestor now works with instance profiles which have paths (#2825) - Fixed a crash in SPIRE Agent caused by a race on the agent cache (#2699) @@ -160,10 +279,12 @@ ## [1.2.0] - 2022-01-28 ### Added + - SPIRE Server can now be configured to mint agent SVIDs with a specific TTL (#2667) - A set of fixed admin SPIFFE IDs can now be configured in SPIRE Server (#2677) ### Changed + - Upstream signed CA chain is now validated to prevent misconfigurations (#2644) - Improved SVID signing logs to include more context (#2678) - The deprecated agent key file (`svid.key`) is no longer proactively removed by the agent (#2671) @@ -171,49 +292,58 @@ - SPIRE now consumes the SVIDStore V1 interface published in the SPIRE Plugin SDK (#2688) ### Deprecated + - API support for paths without leading slashes in `spire.api.types.SPIFFEID` messages has been deprecated (#2686, #2692) -- The SVIDStore V1 interface published in SPIRE repository has been renamed to `svidstore.V1Unofficial` and is now deprecated in favor of the interface published in the SPIRE Plugin SDK (#2688) +- The SVIDStore V1 interface published in SPIRE repository has been renamed to `svidstore.V1Unofficial` and is now deprecated in favor of the interface published in the SPIRE Plugin SDK (#2688) ### Removed + - The deprecated `domain` configurable has been removed from the SPIRE OIDC Discovery Provider (#2672) - The deprecated `allow_unsafe_ids` configurable has been removed from SPIRE Server (#2685) ## [1.1.5] - 2022-05-12 ### Added -- Ability to revert SPIFFE cert validation to standard X.509 validation in Envoy (#3009,#3014,#3020,#3034) +- Ability to revert SPIFFE cert validation to standard X.509 validation in Envoy (#3009,#3014,#3020,#3034) ## [1.1.4] - 2022-04-13 ### Security + - Updated to Go 1.17.9 to address CVE-2022-24675, CVE-2022-28327, CVE-2022-27536 ## [1.1.3] - 2022-01-07 ### Security + - Fixed CVE-2021-44716 ## [1.1.2] - 2021-12-15 ### Added + - SPIRE Agent now supports the Delegated Identity API for delegating SVID management to trusted platform components (#2481) - The K8s Workload Registrar now supports configuring DNS name templates (#2643) - SPIRE Server now logs a message when expired registration entries are pruned (#2637) - OIDC Discovery Provider now supports setting the `use` property on the JWKs it serves (#2634) ### Fixed + - SPIRE Agent now provides reason for failure during certain kinds of attestation errors (#2628) ## [1.1.1] - 2021-11-17 ### Added + - SPIRE Agent can now store SVIDs with Google Cloud Secrets Manager (#2595) ### Changed + - SPIRE Server downloads federated bundles a little sooner when federated relationships are added or updated (#2585) ### Fixed + - Fixed a regression in Percona XTRA DB Cluster support introduced in 0.12.2 (#2605) - Kubernetes Workload Attestation fixed for Kubernetes 1.21+ (#2600) - SPIRE Agent now retries failed removals of SVIDs stored by SVIDStore plugins (#2620) @@ -221,8 +351,9 @@ ## [1.1.0] - 2021-10-10 ### Added + - SPIRE images are now published to GitHub Container Registry. They will continue to be published to Google Container Registry over the course of the next release (#2576,#2580) -- SPIRE Server now implements the [TrustDomain API](https://github.com/spiffe/spire-api-sdk/blob/main/proto/spire/api/server/trustdomain/v1/trustdomain.proto) and related CLI commands (https://github.com/spiffe/spire/projects/11) +- SPIRE Server now implements the [TrustDomain API](https://github.com/spiffe/spire-api-sdk/blob/main/proto/spire/api/server/trustdomain/v1/trustdomain.proto) and related CLI commands () - The SVIDStore plugin type has been introduced to enable, amongst other things, agentless workload scenarios (#2176,#2483) - The TPM DevID Node Attestor emits a new `issuer:cn` selector with the common name of the issuing certificate (#2581) - The K8s Bundle Notifier plugin now supports pushing the bundle to resources in multiple clusters (#2531) @@ -231,13 +362,15 @@ - The GCP CAS UpstreamAuthority has a new `ca_pool` configurable to identify which CA pool the signing CA resides in (#2569) ### Changed + - With the GA release of GCP CAS, the UpstreamAuthority plugin now needs to know which pool the CA belongs to. If not configured, it will do a pessimistic scan of all pools to locate the correct CA. This scan will be removed in a future release (#2569) -- The K8s Workload Registrar now supports Kubernetes 1.22 (#2515,#2540) +- The K8s Workload Registrar now supports Kubernetes 1.22 (#2515,#2540) - Self-signed CA certificates serial numbers are now conformant to RFC 5280 (#2494) - The AWS KMS Key Manager plugin now creates keys with a very strict policy by default (#2424) - The deprecated agent key file (`svid.key`) is proactively removed by the agent. It was only maintained to accomodate rollback from v1.0 to v0.12 (#2493) ### Removed + - Support for the deprecated Registration API has been removed (#2487) - Legacy (v0) plugin support has been removed. All plugins must now be authored using the plugin SDK. - The deprecated `service_account_whitelist` configurables have been removed from the SAT and PSAT Node Attestor plugins (#2543) @@ -245,26 +378,30 @@ - The deprecated `bundle_endpoint` and `registration_uds_path` configurables have been removed from SPIRE Server (#2486,#2519) ### Fixed + - The GCP CAS UpstreamAuthority now works with the GA release of GCP CAS (#2569) - Fixed a variety of issues with the scratch image, preparatory to publishing as the official image on GitHub Container Registry (#2582) - Kubernetes Workload Attestor now uses the canonical path for the service account token (#2583) -- The server socketPath is now appropriately overriden via the configuration file (#2570) +- The server socketPath is now appropriately overridden via the configuration file (#2570) - The server now restarts appropriately after undergoing forceful shutdown (#2496) - The server CLI list commands now work reliably for large listings (#2456) ## [1.0.4] - 2022-05-13 ### Added + - Ability to revert SPIFFE cert validation to standard X.509 validation in Envoy (#3009,#3014,#3020,#3034) ## [1.0.3] - 2022-01-07 ### Security + - Fixed CVE-2021-44716 ## [1.0.2] - 2021-09-02 ### Added + - Experimental support for custom authorization policies based on Open Policy Agent (OPA) (#2416) - SPIRE Server can now be configured to emit audit logs (#2297, #2391, #2394, #2396, #2442, #2458) - Envoy SDS v3 API in agent now supports the SPIFFE Certificate Validator for federated SPIFFE authentication (#2435, #2460) @@ -276,18 +413,22 @@ - Improvements in logging of errors in peertracker (#2469) ### Changed + - CRD mode of the `k8s-workload-registrar` now uses SPIRE certificates for the validating webhook (#2321) - The `vault` UpstreamAuthority plugin now continues retrying to renew tokens on failures until the lease time is exceeded (#2445) ### Fixed + - Fixed a nil pointer dereference when the deprecated `allow_unsafe_ids` setting was configured (#2477) ### Deprecated + - The SPIRE OIDC Discovery Provider `domain` configurable has been deprecated in favor of `domains` (#2404) ## [1.0.1] - 2021-08-05 ### Added + - LDevID-based TPM attestation can now be performed via a new `tpm_devid` NodeAttestor plugin (#2111, #2427) - Caller details are now logged for unauthorized Server API calls (#2399) - The `aws_iid` NodeAttestor plugin now supports attesting nodes across multiple AWS accounts via AWS IAM role assumption (#2387) @@ -296,9 +437,11 @@ - SPIRE Server now logs a message on startup when configured TTL values may result in SVIDs with a shorter lifetime than expected (#2284) ### Changed + - Updated a trust domain validation error message to mention that underscores are valid trust domain characters (#2392) ### Fixed + - Fixed bugs that broke the ACME bundle endpoint when using the `aws_kms` KeyManager plugin (#2390, #2397) - Fixed a bug that resulted in SPIRE Agent sending unnecessary updates over the Workload API (#2305) - Fixed a bug in the `k8s_psat` NodeAttestor plugin that prevented it from being configured with kubeconfig files (#2421) @@ -306,18 +449,20 @@ ## [1.0.0] - 2021-07-08 ### Added + - The `vault` UpstreamAuthority plugin now supports Kubernetes service account authentication (#2356) - A new `cert-manager` UpstreamAuthority plugin is now available (#2274) - SPIRE Server CLI can now be used to ban agents (#2374) - SPIRE Server CLI now has `count` subcommands for agents, entries, and bundles (#2128) - SPIRE Server can now be configured for SPIFFE federation using the configurables defined by the spec (#2340) - SPIRE Server and Agent now expose the standard gRPC health service (#2057, #2058) -- SPIFFE bundle endpoint URL is now configurable in the `federates_with` configuation block (#2340) +- SPIFFE bundle endpoint URL is now configurable in the `federates_with` configuration block (#2340) - SPIRE Agent may now optionally provided unregistered callers with a bundle for SVID validation via the `allow_unauthenticated_verifiers` configurable (#2102) - SPIRE Server JWT key type is now independently configurable via `jwt_key_type` (#1991) - Registration entries can now be queried/filtered by `federates_with` when calling the entry API (#1967) ### Changed + - SPIRE Server's SVID now uses the key type configured as `ca_key_type` (#2269) - Caller address is now logged for agent API calls resulting in an error (#2281) - Agent SVID renewals are now logged by the server at the INFO level (#2309) @@ -328,24 +473,27 @@ - SPIRE Agent default socket path is now `/tmp/spire-agent/public/api.sock` (#2075) ### Deprecated + - SPIRE Server federation configuration in the `federates_with` `bundle_endpoint` block is now deprecated (#2340) - SPIRE Server `gcp_iit` NodeAttestor configurable `projectid_whitelist` is deprecated in favor of `projectid_allow_list` (#2253) - SPIRE Server `k8s_sat` and `k8s_psat` NodeAttestor configurable `service_account_whitelist` is deprecated in favor of `service_account_allow_list` (#2253) -- SPIRE Sever `registration_uds_path`/`-registrationUDSPath` configurable and flag has been deprecateed in favor of `socket_path`/`-socketPath` (#2075) +- SPIRE Server `registration_uds_path`/`-registrationUDSPath` configurable and flag has been deprecated in favor of `socket_path`/`-socketPath` (#2075) ### Removed + - SPIRE Server no longer supports SPIFFE IDs with UTF-8 (#2368) - SPIRE Server no longer supports the legacy Node API (#2093) - SPIRE Server experimental configurable `allow_agentless_node_attestors` has been removed (#2098) - The `aws_iid` NodeResolver plugin has been removed as it has been obviated (#2191) - The `noop` NodeResolver plugin has been removed (#2189) - The `proto/spire` go module has been removed in favor of the new SDKs (#2161) -- The deprected `enable_sds` configurable has been removed (#2021) +- The deprecated `enable_sds` configurable has been removed (#2021) - The deprecated `experimental bundle` CLI subcommands have been removed (#2062) - SPIRE Server experimental configurables related to federation have been removed (#2062) - SPIRE Server bundle endpoint no longer supports TLS signature schemes utilizing non-SHA256 hashes when ACME is enabled (#2397) ### Fixed + - Fixed a bug that caused health check failures in agents that have registration entries describing them (#2370) - SPIRE Agent no longer logs a message when invoking a healthcheck via the CLI (#2058) - Fixed a bug that caused federation to fail when using ACME in conjunction with the `aws_kms` KeyManager plugin (#2390) @@ -353,21 +501,25 @@ ## [0.12.3] - 2021-05-17 ### Added + - The `k8s-workload-registrar` now supports federation (#2160) - The `k8s_bundle` notifier plugin can now keep API service CA bundles up to date (#2193) - SPIRE Server internal cache reload timing can now be tuned (experimental) (#2169) ### Changed + - Prometheus metrics that are emitted infrequently will no longer disappear after emission (#2239) - The `k8s-workload-registrar` now uses paging to support very large deployments of 10,000+ pods (#2227) ### Fixed + - Fixed a bug that sometimes caused newly attested agents to not receive their full set of selectors (#2242) - Fixed several bugs related to the handling of SPIRE Server API paging (#2251) ## [0.12.2] - 2021-04-14 ### Added + - Added `aws_kms` server KeyManager plugin that uses the AWS Key Management Service (KMS) (#2066) - Added `gcp_cas` UpstreamAuthority plugin that uses the Certificate Authority Service from Google Cloud Platform (#2172) - Improved error returned during attestation of agents (#2159) @@ -379,12 +531,14 @@ - Added logging when lookup of user by uid or group by gid fails in the `unix` WorkloadAttestor plugin (#2048) ### Changed + - The `k8s` WorkloadAttestor plugin now emits selectors for both image and image ID (#2116) - HTTP readiness endpoint on agent now checks the health of the Workload API (#2015, #2087) - SDS API in agent now returns an error if an SDS client requests resource names that don't exist (#2020) - Bundle and k8s-workload-registrar endpoints now only accept clients using TLS v1.2+ (#2025) ### Fixed + - Registration entry update handling in CRD mode of the k8s-workload-registrar to prevent unnecessary issuance of new SVIDs (#2155) - Failure to update CA bundle due to improper MySQL isolation level for read-modify-write operations (#2150) - Regression preventing agent selectors from showing in `spire-server agent show` command (#2133) @@ -395,6 +549,7 @@ ## [0.12.1] - 2021-03-04 ### Security + - Fixed CVE-2021-27098 - Fixed CVE-2021-27099 - Fixed file descriptor leak in peertracker @@ -402,6 +557,7 @@ ## [0.12.0] - 2020-12-17 ### Added + - Debug endpoints (#1792) - Agent support for SDS v3 API (#1906) - Improved metrics handling (#1885, #1925, #1932) @@ -416,19 +572,23 @@ - Added `k8s_psat:agent_node_ip` selector (#1979) ### Changed + - The agent now shuts down when it is no longer attested (#1797) - Internals now rely on new server APIs (#1849, #1878, #1907, #1908, #1909, #1913, #1947, #1982, #1998, #2001) - Workload API now returns a standardized JWKS object (#1904) - Log message casing and punctuation are more consistent with project guidelines (#1950, #1952) ### Deprecated + - The Registration and Node APIs are deprecated, and a warning is logged on use (#1997) - The `registration_api` configuration section is deprecated in favor of `server_api` in the k8s-workload-registrar (#2001) ### Removed + - Removed some superfluous or otherwise unusable metrics and labels (#1881, #1946, #2004) ### Fixed + - Fixed CLI exit codes when entry create or update fails (#1990) - Fixed a bug that could cause external plugins to become orphaned processes after agent/server shutdown (#1962) - Fixed handling of the Vault PKI certificate chain (#2012, #2017) @@ -436,11 +596,13 @@ - Fixed Registration API to validate selector syntax (#1919) ### Security + - JWT-SVIDs that fail validation are no longer logged (#1953) ## [0.11.3] - 2021-03-04 ### Security + - Fixed CVE-2021-27098 - Fixed CVE-2021-27099 - Fixed file descriptor leak in peertracker @@ -448,9 +610,11 @@ ## [0.11.2] - 2020-10-29 ### What's New - - Error messages related to a specific class of software bugs are now rate limited (#1901) + +- Error messages related to a specific class of software bugs are now rate limited (#1901) ### What's Changed + - Fixed an issue in the Upstream Authority plugin that could result in a delay in the propagation of bundle updates/changes (#1917) - Fixed error messages when attestation is disabled (#1899) - Fixed some incorrectly-formatted log messages (#1920) @@ -458,24 +622,27 @@ ## [0.11.1] - 2020-09-29 ### What's New + - Added AWS PCA configurable allowing operators to provide additional CA certificates for inclusion in the bundle (#1574) - Added a configurable to server for disabling rate limiting of node attestation requests (#1794, #1870) ### What's Changed + - Fixed Kubernetes Workload Registrar issues (#1814, #1818, #1823) - Fixed BatchCreateEntry return value to match docs, returning the contents of an entry if it already exists (#1824) - Fixed issue preventing brand new deployments from downgrading successfully (#1829) - Fixed a regression introduced in 0.11.0 that caused external node attestor plugins that rely on binary data to fail (#1863) - ## [0.11.0] - 2020-08-28 ### What's New + - Introduced refactored server APIs (#1533, #1548, #1563, #1567, #1568, #1571, #1575, #1576, #1577, #1578, #1582, #1585, #1586, #1587, #1588, #1589, #1590, #1591, #1592, #1593, #1594, #1595, #1597, #1604, #1606, #1607, #1613, #1615, #1617, #1622, #1623, #1628, #1630, #1633, #1641, #1643, #1646, #1647, #1654, #1659, #1667, #1673, #1674, #1683, #1684, #1689, #1690, #1692, #1693, #1694, #1701, #1708, #1727, #1728, #1730, #1733, #1734, #1739, #1749, #1753, #1768, #1772, #1779, #1783, #1787, #1788, #1789, #1790, #1791) - Unix workloads can now be attested using auxiliary group membership (#1771) - The Kubernetes Workload Registrar now supports two new registration modes (`crd` and `reconcile`) ### What's Changed + - Federation is now a stable feature (#1656, #1737, #1777) - Removed support for the `UpstreamCA` plugin, which was deprecated in favor of the `UpstreamAuthority` plugin in v0.10.0 (#1699) - Removed deprecated `upstream_bundle` server configurable. The server now always use the upstream bundle as the trust bundle (#1702) @@ -488,22 +655,26 @@ ## [0.10.2] - 2021-03-04 ### Security + - Fixed CVE-2021-27098 - Fixed file descriptor leak in peertracker ## [0.10.1] - 2020-06-23 ### What's New + - `vault` as Upstream Authority built-in plugin (#1611, #1632) - Improved configuration file docs to list all possible configuration settings (#1608, #1618) ### What's Changed + - Improved container ID parsing from cgroup path in the `docker` workload attestor plugin (#1605) - Improved container ID parsing from cgroup path in the `k8s` workload attestor plugin (#1649) - Envoy SDS support is now always on (#1579) - Errors on agent SVID rotation are now fatal if the agent's current SVID has expired, forcing an agent restart (#1584) ## [0.10.0] - 2020-04-22 + - Added support for JWT-SVID in nested SPIRE topologies (#1388, #1394, #1396, #1406, #1409, #1410, #1411, #1415, #1416, #1417, #1423, #1440, #1455, #1458, #1469, #1476) - Reduced database load under certain configurations (#1439) - Agent now proactively rotates workload SVIDs in response to registration updates (#1441, #1477) @@ -528,10 +699,12 @@ ## [0.9.4] - 2021-03-04 ### Security + - Fixed CVE-2021-27098 - Fixed file descriptor leak in peertracker ## [0.9.3] - 2020-03-05 + - Significantly reduced the server's database load (#1350, #1355, #1397) - Improved consistency in SVID propagation time for some cases (#1352) - AWS IID node attestor now supports the v2 metadata service (#1369) @@ -542,11 +715,13 @@ - Registration API now has an RPC for listing entries that supports paging (#1392) ## [0.9.2] - 2020-01-14 + - Fixed a crash when a key protecting the bundle endpoint is removed (#1326) - Bundle endpoint client now supports Web-PKI authenticated endpoints (#1327) - SPIRE now warns if the CA TTL will result in shorter-than-expected SVID lifetimes (#1294) ## [0.9.1] - 2019-12-19 + - Agent cache file writes are now atomic, more resilient (#1267) - Introduced Google Cloud Storage bundle notifier plugin for server (#1227) - Server and agent now detect unknown configuration options in supported blocks (#1289, #1299, #1306, #1307) @@ -558,10 +733,11 @@ - KeyManager "disk" now emits a friendly error when directory option is missing (#1313) ## [0.9.0] - 2019-11-14 + - Users can now opt-out of workload executable hashing when enabling the workload path as a selector (#1078) - Added M3 support to telemetry and other telemetry and logging improvements (#1059, #1085, #1086, #1094, #1102, #1122,#1138,#1160,#1186,#1208) - SQL auto-migration can be disabled (#1089) -- SQL schema compatability checks are aligned with upgrade compatability guarantees (#1089) +- SQL schema compatibility checks are aligned with upgrade compatibility guarantees (#1089) - Agent CLI can provide information on attested nodes (#1098) - SPIRE can tolerate small SVID expiration periods (#1115) - Reduced Docker image sizes by roughly 25% (#1140) @@ -579,19 +755,23 @@ ## [0.8.5] - 2021-03-04 ### Security + - Fixed CVE-2021-27098 - Fixed file descriptor leak in peertracker ## [0.8.4] - 2019-10-28 + - Fixed spurious agent synchronization failures during agent SVID rotation (#1084) - Added support for [Kind](https://kind.sigs.k8s.io) to the Kubernetes Workload Attestor (#1133) - Added support for ACME v2 to the bundle endpoint (#1187) - Fixed a bug that could result in agent crashes after upgrading to 0.8.2 or newer (#1194) ## [0.8.3] - 2019-10-18 + - Upgrade to Go 1.12.12 in response to CVE-2019-17596 (#1204) ## [0.8.2] - 2019-10-10 + - Connection pool details in SQL DataStore plugin are now configurable (#1028) - SQL DataStore plugin now emits telemetry (#998) - The SPIFFE bundle endpoint now supports serving Web PKI via ACME (#1029) @@ -605,6 +785,7 @@ - Fix bug that resulted in authorized workloads being denied SVIDs (#1103) ## [0.8.1] - 2019-07-19 + - Failure to obtain peer information from a Workload API connection no longer brings down the agent (#946) - Agent now detects expired cached SVID when it starts and will attempt to re-attest instead of failing (#1000) - GCP IIT-based node attestation produces selectors for the project, zone, instance name, tags, service accounts, metadata and labels (#969, #1006, #1012) @@ -633,6 +814,7 @@ - Logs can now be emitted in JSON format (#866) ## [0.8.0] - 2019-05-20 + - Fix a bug in which the agent periodically logged connection errors (#906) - Kubernetes SAT node attestor now supports the TokenReview API (#904) - Agent cache refactored to improve memory management and fix a leak (#863) @@ -641,7 +823,7 @@ - Fix a bug in AWS IID NodeResolver with instance profile lookup (#888) - Improved workload attestation and fixed a security bug related to PID reuse (#886) - New Kubernetes bundle notifier for keeping a bundle configmap up-to-date (#877) -- New plugin type Notifier for programatically taking action on important events (#877) +- New plugin type Notifier for programmatically taking action on important events (#877) - New NodeAttestor based on SSH certificates (#868, #870) - v2 client library for Workload API interaction (#841) - Back-compat bundle management code removed - bundle is now handled correctly (#858, #859) @@ -663,6 +845,7 @@ - UpstreamCA "disk" now supports loading multiple key types (#717) ## [0.7.3] - 2019-02-11 + - Agent can now expose Envoy SDS API for TLS certificate installation rotation (#667) - Agent now automatically creates its configured data dir if it doesn't exist (#678) - Agent panic fixed in the event that rotation is attempted from non-attested node (#684) @@ -737,7 +920,6 @@ - Config file updates so spire commands can be run from any CWD (#541) - Minor doc/example fixes (#535) - ## [0.6.0] - 2018-06-26 - Added GCP Instance Identity Token (IIT) node attestation. diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md index b4a0371d7e..6ef132aba5 100644 --- a/CODE-OF-CONDUCT.md +++ b/CODE-OF-CONDUCT.md @@ -1,8 +1,10 @@ -### Contributor Code of Conduct +# Code of Conduct + +## Contributor Code of Conduct We follow the [CNCF Contributor Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). Additionally, we commit to the following guidelines as detailed on the [SPIFFE Code of Conduct](https://github.com/spiffe/spiffe/blob/main/CODE-OF-CONDUCT.md): -### Community Guidelines +## Community Guidelines - Our goal is to foster an inclusive and diverse community of technology enthusiasts. @@ -14,6 +16,6 @@ We follow the [CNCF Contributor Code of Conduct](https://github.com/cncf/foundat - We do our best to avoid [subtle-isms](https://www.recurse.com/manual#sub-sec-social-rules): small actions that make others feel uncomfortable. If you witness a subtle-ism, you may respectfully point it out to the person publicly or privately, or you may ask a moderator to say something. Accidentally saying something biased is common, expected, and readily forgiven. It is not in and of itself a bannable offense. -### Moderation +## Moderation - If you feel any of SPIFFE's Slack channels require moderation, please e-mail [SPIFFE's Technical Steering Committee (TSC)](mailto:tsc@spiffe.io). The TSC will issue a warning to users who don't follow this code of conduct. A second offense results in a temporary ban. A third offense warrants a permanent ban. It is at the moderator's discretion to un-ban offending users, or to immediately ban a toxic user without warning. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14b489e3e2..fb95a345ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,6 @@ -# Contributor guidelines and Governance +# Contributing + +## Contributor guidelines and Governance Please see [CONTRIBUTING](https://github.com/spiffe/spiffe/blob/main/CONTRIBUTING.md) @@ -6,20 +8,19 @@ and [GOVERNANCE](https://github.com/spiffe/spiffe/blob/main/GOVERNANCE.md) from the SPIFFE project. -# Prerequisites +## Prerequisites For basic development you will need: -* **Go 1.11** or higher (https://golang.org/dl/) +* **Go 1.11** or higher () For development that requires changes to the gRPC interfaces you will need: -* The protobuf compiler (https://github.com/google/protobuf) -* The protobuf documentation generator (https://github.com/pseudomuto/protoc-gen-doc) +* The protobuf compiler () +* The protobuf documentation generator () * protoc-gen-go and protoc-gen-spireplugin (`make utils`) - -# Building +## Building Since go modules are used, this repository can live in any folder on your local disk (it is not required to be in GOPATH). @@ -38,20 +39,20 @@ The Makefile takes care of installing the required toolchain as needed. The toolchain and other build related files are cached under the `.build` folder (ignored by git). -## Development in Docker +### Development in Docker You can either build SPIRE on your host or in a Ubuntu docker container. In both cases you will use the same Makefile commands. To build SPIRE within a container, first build the development image: -``` +```shell $ make dev-image ``` Then launch a shell inside of development container: -``` +```shell $ make dev-shell ``` @@ -59,17 +60,17 @@ Because the docker container shares the `.build` cache and `$GOPATH/pkg/mod` you will not have to re-install the toolchain or go dependencies every time you run the container. -# Conventions +## Conventions In addition to the conventions covered in the SPIFFE project's [CONTRIBUTING](https://github.com/spiffe/spiffe/blob/main/CONTRIBUTING.md), the following conventions apply to the SPIRE repository: -## SQL Plugin Changes +### SQL Plugin Changes Datastore changes must be present in at least one full minor release cycle prior to introducing code changes that depend on them. -## Directory layout +### Directory layout `/cmd/{spire-server,spire-agent}/` @@ -94,7 +95,7 @@ gRPC .proto files, their generated .pb.go, and README_pb.md. The protobuf package names should be `spire.{server,agent,api,common}.` and the go package name should be specified with `option go_package = "";` -## Interfaces +### Interfaces Packages should be exported through interfaces. Interaction with packages must be done through these interfaces @@ -102,7 +103,7 @@ interfaces Interfaces should be defined in their own file, named (in lowercase) after the name of the interface. eg. `foodata.go` implements `type FooData interface{}` -## Metrics +### Metrics As much as possible, label names should be constants defined in the `telemetry` package. Additionally, specific metrics should be centrally defined in the `telemetry` package or its subpackages. Functions @@ -111,13 +112,13 @@ The metrics emitted by SPIRE are listed in the [telemetry document](doc/telemetr In addition, metrics should be unit-tested where reasonable. -### Count in Aggregate +#### Count in Aggregate Event count metrics should aggregate where possible to reduce burden on metric sinks, infrastructure, and consumers. That is, instead of: -``` +```go for ... { if ... { foo.Bar = X @@ -130,7 +131,7 @@ for ... { Change to this instead: -``` +```go updateCount := 0 notUpdatedCount := 0 for ... { @@ -149,16 +150,18 @@ telemetry.FooNotUpdatedCount(notUpdatedCount) Labels added to metrics must be singular only; that is: -- the value of a metrics label must not be an array or slice, and a label of some name must only be added +* the value of a metrics label must not be an array or slice, and a label of some name must only be added once. Failure to follow this will make metrics less usable for non-tagging metrics libraries such as `statsd`. As counter examples, DO NOT do the following: -``` + +```go []telemetry.Label{ {Name: "someName", "val1"}, {Name: "someName", "val2"}, } ``` -``` + +```go var callCounter telemetry.CallCounter ... callCounter.AddLabel("someName", "val1") @@ -166,12 +169,13 @@ callCounter.AddLabel("someName", "val1") callCounter.AddLabel("someName", "val2") ``` -- the existence of a metrics label is constant for all instances of a given metric. For some given metric A with +* the existence of a metrics label is constant for all instances of a given metric. For some given metric A with label X, label X must appear in every instance of metric A rather than conditionally. Failure to follow this will make metrics less usable for non-tagging metrics libraries such as `statsd`, and potentially break aggregation for tagging metrics libraries. As a counter example, DO NOT do the following: -``` + +```go var callCounter telemetry.CallCounter ... if caller != "" { @@ -182,8 +186,10 @@ if x > 5000 { callCounter.AddLabel("big_load", "true") } ``` + Instead, the following would be more acceptable: -``` + +```go var callCounter telemetry.CallCounter ... if caller != "" { @@ -199,7 +205,7 @@ if x > 5000 { } ``` -## Logs and Errors +### Logs and Errors Errors should start with lower case, and logged messages should follow standard casing. @@ -209,7 +215,7 @@ look for and hinders aggregation. Log messages and error messages should not end with periods. -## Mocks v.s. Fakes +### Mocks v.s. Fakes Unit tests should avoid mocks (e.g. those generated via go-mock) and instead prefer fake implementations. Mocks tend to be brittle as they encode specific @@ -223,13 +229,15 @@ implementation can easily serve the needs for an entire suite of tests and the behavior is in a centralized location when it needs to be updated. Fakes are also less inclined to be impacted by changes to usage patterns. -# Git hooks +## Git hooks We have checked in a pre-commit hook which enforces `go fmt` styling. Please install it before sending a pull request. From the project root: +```shell +$ ln -s .githooks/pre-commit .git/hooks/pre-commit ``` -ln -s .githooks/pre-commit .git/hooks/pre-commit -``` -# Reporting security vulnerabilities + +## Reporting security vulnerabilities + If you've found a vulnerability or a potential vulnerability in SPIRE please let us know at security@spiffe.io. We'll send a confirmation email to acknowledge your report, and we'll send an additional email when we've identified the issue positively or negatively. diff --git a/Dockerfile b/Dockerfile index ba6564eaf7..d6156d2447 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,43 +1,37 @@ # Build stage ARG goversion FROM golang:${goversion}-alpine as builder -RUN apk add build-base git mercurial -ADD go.mod /spire/go.mod -RUN cd /spire && go mod download -ADD . /spire WORKDIR /spire +RUN apk --no-cache --update add build-base git mercurial +ADD go.* ./ +RUN go mod download +ADD . . RUN make build # Common base FROM alpine AS spire-base -RUN apk --no-cache add dumb-init -RUN apk --no-cache add ca-certificates +WORKDIR /opt/spire RUN mkdir -p /opt/spire/bin +CMD [] +RUN apk --no-cache --update add dumb-init +RUN apk --no-cache --update add ca-certificates # SPIRE Server FROM spire-base AS spire-server -COPY --from=builder /spire/bin/spire-server /opt/spire/bin/spire-server -WORKDIR /opt/spire ENTRYPOINT ["/usr/bin/dumb-init", "/opt/spire/bin/spire-server", "run"] -CMD [] +COPY --from=builder /spire/bin/spire-server bin/spire-server # SPIRE Agent FROM spire-base AS spire-agent -COPY --from=builder /spire/bin/spire-agent /opt/spire/bin/spire-agent -WORKDIR /opt/spire ENTRYPOINT ["/usr/bin/dumb-init", "/opt/spire/bin/spire-agent", "run"] -CMD [] +COPY --from=builder /spire/bin/spire-agent bin/spire-agent # K8S Workload Registrar FROM spire-base AS k8s-workload-registrar -COPY --from=builder /spire/bin/k8s-workload-registrar /opt/spire/bin/k8s-workload-registrar -WORKDIR /opt/spire ENTRYPOINT ["/usr/bin/dumb-init", "/opt/spire/bin/k8s-workload-registrar"] -CMD [] +COPY --from=builder /spire/bin/k8s-workload-registrar bin/k8s-workload-registrar # OIDC Discovery Provider FROM spire-base AS oidc-discovery-provider -COPY --from=builder /spire/bin/oidc-discovery-provider /opt/spire/bin/oidc-discovery-provider -WORKDIR /opt/spire ENTRYPOINT ["/usr/bin/dumb-init", "/opt/spire/bin/oidc-discovery-provider"] -CMD [] +COPY --from=builder /spire/bin/oidc-discovery-provider bin/oidc-discovery-provider diff --git a/Dockerfile.dev b/Dockerfile.dev index 4b1fd91a07..72099782fd 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,6 +1,4 @@ FROM ubuntu:xenial - +WORKDIR /spire RUN apt-get update && apt-get -y install \ curl unzip git build-essential ca-certificates libssl-dev - -WORKDIR /spire diff --git a/Dockerfile.scratch b/Dockerfile.scratch index 420c3df3e0..9d4a17ef25 100644 --- a/Dockerfile.scratch +++ b/Dockerfile.scratch @@ -1,42 +1,35 @@ # Build stage ARG goversion FROM golang:${goversion}-alpine as builder -RUN apk add build-base git mercurial ca-certificates -RUN apk add --update gcc musl-dev -ADD go.mod /spire/go.mod -RUN cd /spire && go mod download -ADD . /spire WORKDIR /spire +RUN apk add --no-cache --update build-base musl-dev git mercurial ca-certificates +ADD go.* ./ +RUN go mod download +ADD . . RUN make build-static +RUN install -d -o root -g root -m 1777 /newtmp -# SPIRE Server -FROM scratch AS spire-server-scratch -COPY --from=builder /spire/bin/spire-server-static /opt/spire/bin/spire-server -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +FROM scratch AS spire-base WORKDIR /opt/spire -ENTRYPOINT ["/opt/spire/bin/spire-server", "run"] CMD [] - -FROM scratch AS spire-agent-scratch -COPY --from=builder /spire/bin/spire-agent-static /opt/spire/bin/spire-agent COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -WORKDIR /opt/spire -EXPOSE 8080 8443 +COPY --from=builder /newtmp /tmp + +# SPIRE Server +FROM spire-base AS spire-server-scratch +ENTRYPOINT ["/opt/spire/bin/spire-server", "run"] +COPY --from=builder /spire/bin/static/spire-server bin/ + +FROM spire-base AS spire-agent-scratch ENTRYPOINT ["/opt/spire/bin/spire-agent", "run"] -CMD [] +COPY --from=builder /spire/bin/static/spire-agent bin/ # K8S Workload Registrar -FROM scratch AS k8s-workload-registrar-scratch -COPY --from=builder /spire/bin/k8s-workload-registrar-static /opt/spire/bin/k8s-workload-registrar -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -WORKDIR /opt/spire +FROM spire-base AS k8s-workload-registrar-scratch ENTRYPOINT ["/opt/spire/bin/k8s-workload-registrar"] -CMD [] +COPY --from=builder /spire/bin/static/k8s-workload-registrar bin/ # OIDC Discovery Provider -FROM scratch AS oidc-discovery-provider-scratch -COPY --from=builder /spire/bin/oidc-discovery-provider-static /opt/spire/bin/oidc-discovery-provider -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -WORKDIR /opt/spire +FROM spire-base AS oidc-discovery-provider-scratch ENTRYPOINT ["/opt/spire/bin/oidc-discovery-provider"] -CMD [] +COPY --from=builder /spire/bin/static/oidc-discovery-provider bin/ diff --git a/Dockerfile.windows b/Dockerfile.windows index 16748ab50e..bed4c9e314 100644 --- a/Dockerfile.windows +++ b/Dockerfile.windows @@ -4,32 +4,25 @@ FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS spire-base-windows RUN mkdir c:\\spire\\bin RUN mkdir c:\\spire\\data +WORKDIR C:/spire +CMD [] # SPIRE Server FROM spire-base-windows AS spire-server-windows -COPY bin/spire-server.exe C:/spire/bin/spire-server.exe -WORKDIR C:/spire ENTRYPOINT ["c:/spire/bin/spire-server.exe", "run"] -CMD [] +COPY bin/spire-server.exe C:/spire/bin/spire-server.exe # SPIRE Agent FROM spire-base-windows AS spire-agent-windows -COPY ./bin/spire-agent.exe C:/spire/bin/spire-agent.exe -WORKDIR C:/spire ENTRYPOINT ["c:/spire/bin/spire-agent.exe", "run"] -CMD [] +COPY ./bin/spire-agent.exe C:/spire/bin/spire-agent.exe # K8S Workload Registrar FROM spire-base-windows AS k8s-workload-registrar-windows -COPY ./bin/k8s-workload-registrar.exe C:/spire/bin/k8s-workload-registrar.exe -WORKDIR c:/spire ENTRYPOINT ["c:/spire/bin/k8s-workload-registrar.exe"] -CMD [] +COPY ./bin/k8s-workload-registrar.exe C:/spire/bin/k8s-workload-registrar.exe # OIDC Discovery Provider FROM spire-base-windows AS oidc-discovery-provider-windows -COPY ./bin/oidc-discovery-provider.exe c:/spire/bin/oidc-discovery-provider.exe -WORKDIR c:/spire ENTRYPOINT ["c:/spire/bin/oidc-discovery-provider.exe"] -CMD [] - +COPY ./bin/oidc-discovery-provider.exe c:/spire/bin/oidc-discovery-provider.exe diff --git a/MAINTAINERS.md b/MAINTAINERS.md index a7c8d6f1d3..d6cfe5ac3b 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,4 +1,5 @@ # SPIRE Maintainership Guidelines and Processes + This document captures the values, guidelines, and processes that the SPIRE project and its maintainers adhere to. All SPIRE maintainers, in their independent and individual capacity, agree to uphold and abide by the text contained herein. This process can be changed, either permanently or as a one-time exception, through an 80% supermajority maintainer vote. @@ -6,18 +7,22 @@ This process can be changed, either permanently or as a one-time exception, thro For a list of active SPIRE maintainers, please see the [CODEOWNERS](CODEOWNERS) file. ## General Governance + The SPIRE project abides by the same [governance procedures][1] as the SPIFFE project, and ultimately reports to the SPIFFE TSC the same way that the SPIFFE project and associated maintainers do. TSC members do not track day-to-day activity in the SPIFFE/SPIRE projects, and this should be considered when deciding to raise issues to them. While the SPIFFE TSC has the ultimate say, in practice they are only engaged upon serious maintainer disagreement. To say that this would be unprecedented is an understatement. ### Maintainer Responsibility + SPIRE maintainers adhere to the [requirements and responsibilities][2] set forth in the SPIFFE governance document. They further pledge the following: + * To act in the best interest of the project at all times. * To ensure that project development and direction is a function of community needs. * To never take any action while hesitant that it is the right action to take. * To fulfill the responsibilities outlined in this document and its dependents. ### Number of Maintainers + The SPIRE project keeps a total of five maintainer seats. This number was chosen because 1) it results in a healthy distribution of responsibility/load given the current volume of project activity, and 2) an odd number is highly desirable for dispute resolution. We strive to keep the number of maintainers as low as is reasonably possible, given the fact that maintainers carry powerful privileges. @@ -25,6 +30,7 @@ We strive to keep the number of maintainers as low as is reasonably possible, gi This section of the document can and should be updated as the above considerations fluctuate. Changes to this section of the document fall under the same requirements as other sections. When changing this section, maintainers must re-review and agree with the document in its entirety, as other guidelines (e.g. voting requirements) will likely change as a result. ### Changes in Maintainership + SPIRE maintainers are appointed according to the [process described in the governance document][2]. Maintainers may voluntarily step down at any time. Unseating a maintainer against their will requires a unanimous vote with the exception of the unseated. Unseating a maintainer is an extraordinary circumstance. A process to do so is necessary, but its use is not intended. Careful consideration should be made when voting in a new maintainer, particularly in validating that they pledge to uphold the terms of this document. To ensure that these decisions are not taken lightly, and to maintain long term project stability and foresight, no more than one maintainer can be involuntarily unseated in any given nine month period. @@ -32,11 +38,13 @@ Unseating a maintainer is an extraordinary circumstance. A process to do so is n The CNCF MUST be notified of any changes in maintainership via the CNCF Service Desk. #### Onboarding a New Maintainer + New SPIRE maintainers participate in an onboarding period during which they fulfill all code review and issue management responsibilities that are required for their role. The length of this onboarding period is variable, and is considered complete once both the existing maintainers and the candidate maintainer are comfortable with the candidate's competency in the responsibilities of maintainership. This process MUST be completed prior to the candidate being named an official SPIRE maintainer. The onboarding period is intended to ensure that the to-be-appointed maintainer is able/willing to take on the time requirements, familiar with SPIRE core logic and concepts, understands the overall system architecture and interactions that comprise it, and is able to work well with both the existing maintainers and the community. ## Change Review and Disagreements + The SPIRE project abides by the same [change review process][3] as the SPIFFE project, unless otherwise specified. The exact definition/difference between "major" and "minor" changes is left to maintainer's discretion. Changes to particularly sensitive areas like the agent's cache manager, or the server's CA, are always good candidates for additional review. If in doubt, always ask for another review. @@ -44,6 +52,7 @@ The exact definition/difference between "major" and "minor" changes is left to m If there is a disagreement amongst maintainers over a contribution or proposal, a vote may be called in which a simple majority wins. If any maintainer feels that the result of this vote critically endangers the project or its users, they have the right to raise the matter to the SPIFFE TSC. If this occurs, the contribution or proposal in question MUST be frozen until the SPIFFE TSC has made a decision. Do not take this route lightly (see [General Governance](#general-governance)). ### Security and Usability + SPIRE solves a complicated problem, and is developed and maintained by people with deep expertise. SPIRE maintainers must ensure that new features, log and error messages, documentation and naming choices, are all easily accessible by those who may not be very familiar with SPIFFE or authentication systems in general. Decisions should favor "secure by default" and "it just works" anywhere possible, and in that order. The number of configurables should be minimized as much as possible, especially in cases where it's believed that many users would need to invoke it, or when their values (and extremes) could significantly affect SPIRE performance, reliability, or security. @@ -51,9 +60,11 @@ Decisions should favor "secure by default" and "it just works" anywhere possible A good measure is the "beginner" measure. A beginner should be able to easily and quickly understand the configurable/feature, and its potential uses/impacts. They should also be able to easily and quickly troubleshoot a problem when something important goes wrong - and not to mention, be clearly informed of such a condition! ### Review Guidelines + The SPIFFE [governance document][1], its section on [review process][3], and the SPIRE [contribution guidelines][4], must all be applied for any SPIRE review. While reviewing, SPIRE maintainers should ask questions similar to the following: + * Do I clearly understand the use case that this change is addressing? * Does the proposed change break any current user's expectations of behavior (i.e. regression)? * Is it possible for this change to be misconfigured? If it is, what is the impact? @@ -65,18 +76,27 @@ While reviewing, SPIRE maintainers should ask questions similar to the following The above list is advisory, and is meant only to get the mind going. ## Release and Branch Management + The SPIRE project maintains active support for both the current and the previous major versions. All active development occurs in the `main` branch. Version branches are used for minor releases of the previous major version when necessary. ### Version Branches -When a bug is discovered in the latest release that also affects releases of the prior major version, it is necessary to backport the fix. -If it is the first time that the prior major version is receiving a backported patch, then a version branch is created to track it. The version branch is named `vX.Y` where X and Y are the two most significant digits in the semantic version number. Its base is the last tag present in main for the release in question. For example, if SPIRE is on version 0.9.3, and the last 0.8 release was 0.8.4, then a `v0.8` branch is created with its base being the main commit tagged with `v0.8.4`. +Each release must have its own release branch following the naming convention `release/vX.Y.Z` where `X` is the major version, `Y` is the minor version, and `Z` is patch version. + +The base commit of the release branch is based on the type of release being generated: + +* Patch release for older minor release series. In this case, the new release branch is based off of the previous patch release branch for the same minor release series. Example: the latest release is v1.5.z, and the release being prepared is v1.4.5. The base commit should be the `release/v1.4.4` branch. +* Security release for current minor release series. In this case, the new release branch should be based off of the previous release branch for the same minor release series. Example: the latest release is v1.5.0, and the release being prepared is v1.5.1. The base commit should be the `release/v1.5.0` branch. +* Scheduled patch release for current minor release series OR scheduled minor release. In this case, the new release branch should be based off of a commit on the `main` branch. Example: the latest release is v1.5.0, and the release being prepared is v1.5.1. The base commit should be the candidate commit selected from the `main` branch. + +When a bug is discovered in the latest release that also affects releases of the prior minor version, it is necessary to backport the fix. Once the version branch is created, the patch is either cherry picked or backported into a PR against the version branch. The version branch is maintained via the same process as the main branch, including PR approval process etc. -Releases for the previous major version are made directly from its version branch. Ensure that the CHANGELOG is updated in both the main and the version branch to reflect the new release. +Ensure that the CHANGELOG is updated in both `main` and the version branch to reflect the new release. ### Releasing + The SPIRE release machinery is tag-driven. When the maintainers are ready to release, a tag is pushed referencing the release commit. While the CI/CD pipeline takes care of the rest, it is important to keep an eye on its progress. If an error is encountered during this process, the release is aborted. The first two releases that a new maintainer performs must be performed under the supervision of maintainer that has already satisfied this requirement. @@ -86,55 +106,64 @@ SPIRE releases are authorized by its maintainers. When doing so, they should car A simple majority vote is required to authorize a SPIRE release at a specific commit hash. If any maintainer feels that the result of this vote critically endangers the project or its users, they have the right to raise the matter to the SPIFFE TSC. If this occurs, the release in question MUST be frozen until the SPIFFE TSC has made a decision. Do not take this route lightly (see [General Governance](#general-governance)). #### Checklist + This section summarizes the steps necessary to execute a SPIRE release. Unless explicitly stated, the below steps must be executed in order. -The following steps must be completed one week prior to release: -* Ensure all changes intended to be included in the release are fully merged. +The following steps must be completed by the primary on-call maintainer one week prior to release: + +* Ensure all changes intended to be included in the release are fully merged. For the spire-api-sdk and spire-plugin-sdk repositories, ensure that all changes intended for the upcoming release are merged into the main branch from the next branch. * Identify a specific commit as the release candidate. -* Create a draft pull request against the main branch with the updates to the CHANGELOG following [these guidelines](doc/changelog_guidelines.md). This allows those tracking the project to have early visibility into what will be included in the upcoming release and an opportunity to provide feedback. The release date can be set as "TBD" while it is a draft. -* Raise an issue "Release SPIRE X.Y.Z", and include the release candidate commit hash. Reference the pull request with the updates to the CHANGELOG. +* Raise an issue "Release SPIRE X.Y.Z", and include the release candidate commit hash. +* Create the release branch following the guidelines described in [Version branches](#version-branches). * If the current state of the main branch has diverged from the candidate commit due to other changes than the ones from the CHANGELOG: - * If there is not a version branch for this release, create a branch following the guidelines described in [Version branches](#version-branches). - * Create a GitHub project named `Release vX.X.X` to identify the PRs that will be cherry-picked. The project should have two statuses to track the progress: one to identify the PRs to be cherry-picked and one for those that have been merged in the version branch. * Make sure that the [version in the branch](pkg/common/version/version.go) has been bumped to the version that is being released and that the [upgrade integration test is updated](test/integration/suites/upgrade/README.md#maintenance). - * Cherry-pick into the version branch the commits for all the changes that must be included in the release. + * Cherry-pick into the version branch the commits for all the changes that must be included in the release. Ensure the PRs for these commits all target the release milestone in GitHub. +* Create a draft pull request against the release branch with the updates to the CHANGELOG following [these guidelines](doc/changelog_guidelines.md). This allows those tracking the project to have early visibility into what will be included in the upcoming release and an opportunity to provide feedback. The release date can be set as "TBD" while it is a draft. + +**If this is a major or minor release**, the following steps must be completed by the secondary on-call maintainer at least one day before releasing: -**If this is a major release**, the following steps must be completed before releasing: * Review and exercise all examples in spiffe.io and spire-examples repo against the release candidate hash. * Raise a PR for every example that updates included text and configuration to reflect current state and best practice. * Do not merge this PR yet. It will be updated later to use the real version pin rather than the commit hash. * If anything unusual is encountered during this process, a comment MUST be left on the release issue describing what was observed. -The following steps must be completed to perform a release: -* Mark the pull request to update the CHANGELOG as "Ready for review". Make sure that it is updated with the final release date. **At least two approvals from maintainers are required in order to be able to merge it**. If a version branch was created for the realease, cherry-pick the final CHANGELOG changes into the version branch once they are merged. -* If releasing from main and the current state of the main branch has diverged from the candidate commit due to just the CHANGELOG changes, the candidate commit is now the one that includes the updated CHANGELOG. If releasing from a version branch, the candidate commit is now the one that has the CHANGELOG changes cherry-picked in the branch. -* Cut an annotated tag against the release candidate named `vX.X.X`, where `X.X.X` is the semantic version number of SPIRE. - * The first line of the annotation should be `vX.X.X` followed by the CHANGELOG. **There should be a newline between the version and the CHANGELOG**. +The following steps must be completed by the primary on-call maintainer to perform a release: + +* Mark the pull request to update the CHANGELOG as "Ready for review". Make sure that it is updated with the final release date. **At least two approvals from maintainers are required in order to be able to merge it**. +* Cut an annotated tag against the release candidate named `vX.Y.Z`, where `X.Y.Z` is the semantic version number of SPIRE. + * The first line of the annotation should be `vX.Y.Z` followed by the CHANGELOG. **There should be a newline between the version and the CHANGELOG**. The tag should not contain the Markdown header formatting because the "#" symbol is interpreted as a comment by Git. * Push the annotated tag to SPIRE, and watch the build to completion. * If the build fails, or anything unusual is encountered, abort the release. * Ensure that the GitHub release, container images, and release artifacts are deleted/rolled back if necessary. -* Visit the releases page on GitHub, copy the release notes, click edit and paste them back in. This works around a GitHub rendering bug that you will notice before completing this task. -* Close the GitHub project created to track the release process. +* Visit the releases page on GitHub, copy the release notes, click edit and paste them back in. This works around a GitHub Markdown rendering bug that you will notice before completing this task. +* Create Git tags (not annotated) with the name `vX.Y.Z` in the [spire-api-sdk](https://github.com/spiffe/spire-api-sdk) and [spire-plugin-sdk](https://github.com/spiffe/spire-plugin-sdk) repositories for the HEAD commit of the main branch. +* Open a PR targeted for the main branch that cherry-picks the changelog commit from the latest release so that the changelog on the main branch contains all the release notes. +* Close the GitHub issue created to track the release process. +* Broadcast news of release to the community via available means: SPIFFE Slack, Twitter, etc. * Open and merge a PR to bump the SPIRE version to the next projected version and [update the upgrade integration test](test/integration/suites/upgrade/README.md#maintenance). * For example, after releasing 0.10.0, update the version to 0.10.1, since it is more likely to be released before 0.11.0. * Ideally, this is the first commit merged following the release. +* Create a new GitHub milestone for the next release, if not already created. + +**If this is a major or minor release**, the following steps must be completed by the secondary on-call maintainer no later than one week after the release: -**If this is a major release**, the following steps must be completed no later than one week after the release: * PRs to update spiffe.io and spire-examples repo to the latest major version must be merged. * Ensure that the PRs have been updated to use the version tag instead of the commit sha. -* Broadcast news of release to the community via available means: SPIFFE Slack, Twitter, etc. ## Community Interaction and Presence + Maintainers represent the front line of SPIFFE and SPIRE community engagement. They are the ones interacting with end users on issues, and with contributors on their PRs. SPIRE maintainers must make themselves available to the community. It is critical that maintainers engage in this capacity - for understanding user needs and pains, for ensuring success in project adoption and deployment, and to close feedback loops on recently-introduced changes or features... to name a few. PR and Issue management/response is a critical responsibility for all SPIRE maintainers. In addition, maintainers should, whenever possible: + * Be generally available on the SPIFFE Slack, and engage in questions/conversations raised in the #help and #spire channels. * Attend SPIFFE/SPIRE community events (physically or virtually). * Present SPIFFE/SPIRE at meetups and industry conferences. ### Communication Values + SPIRE maintainers always engage in a respectful and constructive manner, and always follow the [SPIFFE Code of Conduct][6]. It is very important for maintainers to understand that contributions are generally acts of generosity, whether it be creating an issue or sending a pull request. It takes time to do these things. In the vast majority of cases, the motivating factor for taking the time to do this is either to improve the quality of the project for others, or to enable the project to (more easily?) solve a problem that it could not previously. Both of these factors are positive. @@ -165,17 +194,16 @@ The product manager must: The product manager makes the same pledge as maintainers do to act in the best interest at all times and its seat follows the same change guidelines as maintainer seats as described in the governance document. Unseating a product manager against their will requires a unanimous vote by the maintainers. - ## Community Facilitation and Outreach -The project designates a community chair to work with the product manager seat to focus on growing awareness of the project and increasing community engagement. In this role, the community chair is responsible for community outreach and outbound communication. +The project designates a community chair to work with the product manager seat to focus on growing awareness of the project and increasing community engagement. In this role, the community chair is responsible for community outreach and outbound communication. The responsibilities of the community chair are as follows: * Maintain, share with the community and execute a plan for proposed marketing and community outreach activities every release cycle. * Coordinate and facilitate community events (online and in-person). * Maintain and manage the spiffe.io website, ensuring that it stays available and up-to-date. -* Coordinate social media communications. +* Coordinate social media communications. * Ensure that all community events and meetings are recorded, and make the recordings available and discoverable on YouTube. * Ensure that all community meeting notes, discussions, and designs are easily discoverable on Google Docs. * Encourage use of project official channels for all technical and non-technical discussions. @@ -183,8 +211,6 @@ The responsibilities of the community chair are as follows: * Protect the privacy and confidentiality of non-public community information, including personal contact information such as email addresses and phone numbers. * Onboard contributors and welcome them into the community. - - [1]: https://github.com/spiffe/spiffe/blob/main/GOVERNANCE.md [2]: https://github.com/spiffe/spiffe/blob/main/GOVERNANCE.md#maintainers [3]: https://github.com/spiffe/spiffe/blob/main/GOVERNANCE.md#change-review-process diff --git a/Makefile b/Makefile index c65d333edb..af3c17b903 100644 --- a/Makefile +++ b/Makefile @@ -38,8 +38,13 @@ help: @echo " support 'SUITES' variable for executing specific tests" @echo " e.g. SUITES='windows-suites/windows-workload-attestor' make integration-windows" @echo - @echo "$(bold)Build and test:$(reset)" - @echo " $(cyan)all$(reset) - build all SPIRE binaries, lint the code, and run unit tests" + @echo "$(bold)Lint:$(reset)" + @echo " $(cyan)lint$(reset) - lint the code and markdown files" + @echo " $(cyan)lint-code$(reset) - lint the code" + @echo " $(cyan)lint-md$(reset) - lint markdown files" + @echo + @echo "$(bold)Build, lint and test:$(reset)" + @echo " $(cyan)all$(reset) - build all SPIRE binaries, run linters and unit tests" @echo @echo "$(bold)Docker image:$(reset)" @echo " $(cyan)images$(reset) - build all SPIRE Docker images" @@ -106,6 +111,8 @@ endif # Vars ############################################################################ +binaries := spire-server spire-agent oidc-discovery-provider k8s-workload-registrar + build_dir := $(DIR)/.build/$(os1)-$(arch1) go_version_full := $(shell cat .go-version) @@ -129,6 +136,9 @@ golangci_lint_dir = $(build_dir)/golangci_lint/$(golangci_lint_version) golangci_lint_bin = $(golangci_lint_dir)/golangci-lint golangci_lint_cache = $(golangci_lint_dir)/cache +markdown_lint_version = v0.32.2 +markdown_lint_image = ghcr.io/igorshubovych/markdownlint-cli:$(markdown_lint_version) + protoc_version = 3.20.1 ifeq ($(os1),windows) protoc_url = https://github.com/protocolbuffers/protobuf/releases/download/v$(protoc_version)/protoc-$(protoc_version)-win64.zip @@ -168,7 +178,7 @@ protos := \ api-protos := \ plugin-protos := \ - proto/spire/common/plugin/plugin.proto + proto/spire/common/plugin/plugin.proto service-protos := \ @@ -201,7 +211,11 @@ endif ############################################################################ # Flags passed to all invocations of go test -go_test_flags := -timeout=60s +go_test_flags := +ifeq ($(NIGHTLY),) + # Cap unit-test timout to 60s unless we're running nightlies. + go_test_flags += -timeout=60s +endif go_flags := ifneq ($(GOPARALLEL),) @@ -226,54 +240,51 @@ ifeq ($(git_dirty),) go_ldflags += -X github.com/spiffe/spire/pkg/common/version.githash=$(git_hash) endif endif -go_ldflags := '${go_ldflags}' ############################################################################# # Build Targets ############################################################################# .PHONY: build +build: tidy $(addprefix bin/,$(binaries)) -build: tidy bin/spire-server bin/spire-agent bin/k8s-workload-registrar bin/oidc-discovery-provider +go_build := $(go_path) go build $(go_flags) -ldflags '$(go_ldflags)' -o -define binary_rule -.PHONY: $1 -$1: | go-check bin/ - @echo Building $1... - $(E)$(go_path) go build $$(go_flags) -ldflags $$(go_ldflags) -o $1$(exe) $2 -endef +bin/%: cmd/% FORCE | go-check + @echo Building $@… + $(E)$(go_build) $@$(exe) ./$< -# main SPIRE binaries -$(eval $(call binary_rule,bin/spire-server,./cmd/spire-server)) -$(eval $(call binary_rule,bin/spire-agent,./cmd/spire-agent)) -$(eval $(call binary_rule,bin/k8s-workload-registrar,./support/k8s/k8s-workload-registrar)) -$(eval $(call binary_rule,bin/oidc-discovery-provider,./support/oidc-discovery-provider)) +bin/%: support/% FORCE | go-check + @echo Building $@… + $(E)$(go_build) $@$(exe) ./$< -bin/: - @mkdir -p $@ +bin/%: support/k8s/% FORCE | go-check + @echo Building $@… + $(E)$(go_build) $@$(exe) ./$< ############################################################################# # Build Static binaries for scratch docker images ############################################################################# .PHONY: build-static +# The build-static is intended to statically link to musl libc. +# There are possibilities of unexpected errors when statically link to GLIBC. +# https://7thzero.com/blog/golang-w-sqlite3-docker-scratch-image +build-static: tidy $(addprefix bin/static/,$(binaries)) -build-static: tidy bin/spire-server-static bin/spire-agent-static bin/k8s-workload-registrar-static bin/oidc-discovery-provider-static +go_build_static := $(go_path) go build $(go_flags) -ldflags '$(go_ldflags) -linkmode external -extldflags "-static"' -o -# https://7thzero.com/blog/golang-w-sqlite3-docker-scratch-image -define binary_rule_static -.PHONY: $1 -$1: | go-check bin/ - @echo Building $1... - $(E)$(go_path) CGO_ENABLED=1 go build $$(go_flags) -ldflags '-s -w -linkmode external -extldflags "-static"' -o $1$(exe) $2 +bin/static/%: cmd/% FORCE | go-check + @echo Building $@… + $(E)$(go_build_static) $@$(exe) ./$< -endef +bin/static/%: support/% FORCE | go-check + @echo Building $@… + $(E)$(go_build_static) $@$(exe) ./$< -# static builds -$(eval $(call binary_rule_static,bin/spire-server-static,./cmd/spire-server)) -$(eval $(call binary_rule_static,bin/spire-agent-static,./cmd/spire-agent)) -$(eval $(call binary_rule_static,bin/k8s-workload-registrar-static,./support/k8s/k8s-workload-registrar)) -$(eval $(call binary_rule_static,bin/oidc-discovery-provider-static,./support/oidc-discovery-provider)) +bin/static/%: support/k8s/% FORCE | go-check + @echo Building $@… + $(E)$(go_build_static) $@$(exe) ./$< ############################################################################# # Test Targets @@ -295,13 +306,6 @@ else $(E)$(go_path) go test $(go_flags) $(go_test_flags) -race ./... endif -ci-race-test: | go-check -ifneq ($(COVERPROFILE),) - $(E)SKIP_FLAKY_TESTS_UNDER_RACE_DETECTOR=1 $(go_path) go test $(go_flags) $(go_test_flags) -race -count=1 -coverprofile="$(COVERPROFILE)" ./... -else - $(E)SKIP_FLAKY_TESTS_UNDER_RACE_DETECTOR=1 $(go_path) go test $(go_flags) $(go_test_flags) -race -count=1 ./... -endif - integration: ifeq ($(os1), windows) $(error Integration tests are not supported on windows) @@ -325,82 +329,50 @@ artifact: build # Docker Images ############################################################################# -.PHONY: images -images: spire-server-image spire-agent-image k8s-workload-registrar-image oidc-discovery-provider-image - -.PHONY: spire-server-image -spire-server-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target spire-server -t spire-server . - docker tag spire-server:latest spire-server:latest-local +define image_rule +.PHONY: $1 +$1: $3 + echo Building docker image $2 $(PLATFORM)… + $(E)docker build \ + --build-arg goversion=$(go_version_full) \ + --target $2 \ + -t $2 -t $2:latest-local \ + -f $3 \ + . -.PHONY: spire-agent-image -spire-agent-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target spire-agent -t spire-agent . - docker tag spire-agent:latest spire-agent:latest-local +endef -.PHONY: k8s-workload-registrar-image -k8s-workload-registrar-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target k8s-workload-registrar -t k8s-workload-registrar . - docker tag k8s-workload-registrar:latest k8s-workload-registrar:latest-local +.PHONY: images +images: $(addsuffix -image,$(binaries)) -.PHONY: oidc-discovery-provider-image -oidc-discovery-provider-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target oidc-discovery-provider -t oidc-discovery-provider . - docker tag oidc-discovery-provider:latest oidc-discovery-provider:latest-local +$(eval $(call image_rule,spire-server-image,spire-server,Dockerfile)) +$(eval $(call image_rule,spire-agent-image,spire-agent,Dockerfile)) +$(eval $(call image_rule,k8s-workload-registrar-image,k8s-workload-registrar,Dockerfile)) +$(eval $(call image_rule,oidc-discovery-provider-image,oidc-discovery-provider,Dockerfile)) ############################################################################# # Docker Images FROM scratch ############################################################################# .PHONY: scratch-images -scratch-images: spire-server-scratch-image spire-agent-scratch-image k8s-workload-registrar-scratch-image oidc-discovery-provider-scratch-image - -.PHONY: spire-server-scratch-image -spire-server-scratch-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target spire-server-scratch -t spire-server-scratch -f Dockerfile.scratch . - docker tag spire-server-scratch:latest spire-server-scratch:latest-local +scratch-images: $(addsuffix -scratch-image,$(binaries)) -.PHONY: spire-agent-scratch-image -spire-agent-scratch-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target spire-agent-scratch -t spire-agent-scratch -f Dockerfile.scratch . - docker tag spire-agent-scratch:latest spire-agent-scratch:latest-local - -.PHONY: k8s-workload-registrar-scratch-image -k8s-workload-registrar-scratch-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target k8s-workload-registrar-scratch -t k8s-workload-registrar-scratch -f Dockerfile.scratch . - docker tag k8s-workload-registrar-scratch:latest k8s-workload-registrar-scratch:latest-local - -.PHONY: oidc-discovery-provider-scratch-image -oidc-discovery-provider-scratch-image: Dockerfile - docker build --build-arg goversion=$(go_version_full) --target oidc-discovery-provider-scratch -t oidc-discovery-provider-scratch -f Dockerfile.scratch . - docker tag oidc-discovery-provider-scratch:latest oidc-discovery-provider-scratch:latest-local +$(eval $(call image_rule,spire-server-scratch-image,spire-server-scratch,Dockerfile.scratch)) +$(eval $(call image_rule,spire-agent-scratch-image,spire-agent-scratch,Dockerfile.scratch)) +$(eval $(call image_rule,k8s-workload-registrar-scratch-image,k8s-workload-registrar-scratch,Dockerfile.scratch)) +$(eval $(call image_rule,oidc-discovery-provider-scratch-image,oidc-discovery-provider-scratch,Dockerfile.scratch)) ############################################################################# -# Docker Images +# Windows Docker Images ############################################################################# .PHONY: images-windows -images-windows: spire-server-image-windows spire-agent-image-windows oidc-discovery-provider-image-windows - -.PHONY: spire-server-image-windows -spire-server-image-windows: Dockerfile - docker build -f Dockerfile.windows --target spire-server-windows -t spire-server-windows . - docker tag spire-server-windows:latest spire-server-windows:latest-local - -.PHONY: spire-agent-image-windows -spire-agent-image-windows: Dockerfile - docker build -f Dockerfile.windows --target spire-agent-windows -t spire-agent-windows . - docker tag spire-agent-windows:latest spire-agent-windows:latest-local - -.PHONY: k8s-workload-registrar-image-windows -k8s-workload-registrar-image-windows: Dockerfile - docker build -f Dockerfile.windows --target k8s-workload-registrar-windows -t k8s-workload-registrar-windows . - docker tag k8s-workload-registrar-windows:latest k8s-workload-registrar-windows:latest-local +images-windows: $(addsuffix -windows-image,$(binaries)) -.PHONY: oidc-discovery-provider-image-windows -oidc-discovery-provider-image-windows: Dockerfile - docker build -f Dockerfile.windows --target oidc-discovery-provider-windows -t oidc-discovery-provider-windows . - docker tag oidc-discovery-provider-windows:latest oidc-discovery-provider-windows:latest-local +$(eval $(call image_rule,spire-server-windows-image,spire-server-windows,Dockerfile.windows)) +$(eval $(call image_rule,spire-agent-windows-image,spire-agent-windows,Dockerfile.windows)) +$(eval $(call image_rule,k8s-workload-registrar-windows-image,k8s-workload-registrar-windows,Dockerfile.windows)) +$(eval $(call image_rule,oidc-discovery-provider-windows-image,oidc-discovery-provider-windows,Dockerfile.windows)) ############################################################################# # Code cleanliness @@ -420,11 +392,13 @@ endif @echo "Ensuring git repository is clean..." $(E)$(MAKE) git-clean-check -lint: lint-code +lint: lint-code lint-md lint-code: $(golangci_lint_bin) $(E)PATH="$(go_bin_dir):$(PATH)" GOLANGCI_LINT_CACHE="$(golangci_lint_cache)" $(golangci_lint_bin) run ./... +lint-md: + $(E)docker run --rm -v "$(DIR):/workdir" $(markdown_lint_image) "**/*.md" ############################################################################# # Code Generation diff --git a/README.md b/README.md index 2047f2cd37..2a2bc2c98e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ SPIRE (the [SPIFFE](https://github.com/spiffe/spiffe) Runtime Environment) is a toolchain of APIs for establishing trust between software systems across a wide variety of hosting platforms. SPIRE exposes the [SPIFFE Workload API](https://github.com/spiffe/go-spiffe/blob/main/v2/proto/spiffe/workload/workload.proto), which can attest running software systems and issue [SPIFFE IDs](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md) and [SVID](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md)s to them. This in turn allows two workloads to establish trust between each other, for example by establishing an mTLS connection or by signing and verifying a JWT token. SPIRE can also enable workloads to securely authenticate to a secret store, a database, or a cloud provider service. - - [Get SPIRE](#get-spire) - [Learn about SPIRE](#learn-about-spire) - [Integrate with SPIRE](#integrate-with-spire) @@ -16,13 +15,12 @@ SPIRE (the [SPIFFE](https://github.com/spiffe/spiffe) Runtime Environment) is a - [Further Reading](#further-reading) - [Security](#security) - - SPIRE is a [graduated](https://www.cncf.io/projects/spire/) project of the [Cloud Native Computing Foundation](https://cncf.io) (CNCF). If you are an organization that wants to help shape the evolution of technologies that are container-packaged, dynamically-scheduled and microservices-oriented, consider joining the CNCF. ## Get SPIRE - Pre-built releases of SPIRE can be found at [https://github.com/spiffe/spire/releases](https://github.com/spiffe/spire/releases). These releases contain both SPIRE Server and SPIRE Agent binaries. +- Container images are published for [spire-server](https://ghcr.io/spiffe/spire-server), [spire-agent](https://ghcr.io/spiffe/spire-agent), and [oidc-discovery-provider](https://ghcr.io/spiffe/spire-oidc-provider). - Alternatively, you can [build SPIRE from source](/CONTRIBUTING.md). ## Learn about SPIRE @@ -46,7 +44,7 @@ For supported integration versions, see [Supported Integrations](/doc/supported_ ## Contribute to SPIRE The SPIFFE community maintains the SPIRE project. Information on the various SIGs and relevant standards can be found in -https://github.com/spiffe/spiffe. +. - See [CONTRIBUTING](https://github.com/spiffe/spire/blob/main/CONTRIBUTING.md) to get started. - Use [GitHub Issues](https://github.com/spiffe/spire/issues) to request features or file bugs. @@ -71,3 +69,5 @@ A third party security firm ([Cure53](https://cure53.de/)) completed a security ### Reporting Security Vulnerabilities If you've found a vulnerability or a potential vulnerability in SPIRE please let us know at security@spiffe.io. We'll send a confirmation email to acknowledge your report, and we'll send an additional email when we've identified the issue positively or negatively. + + diff --git a/ROADMAP.md b/ROADMAP.md index 160dbc196a..033907e91d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,7 @@ -**Recently completed** +# Roadmap + +## Recently completed + * Use SPIRE on workloads [running on platforms where installing an agent is not possible](https://github.com/spiffe/spire/projects/9) (New!) * Provide an [API](https://github.com/spiffe/spire-api-sdk/blob/main/proto/spire/api/server/trustdomain/v1/trustdomain.proto) on SPIRE Server to allow programmatic configuration of federation relationships (New!) * [API](https://github.com/spiffe/spire-api-sdk) and [Plugin](https://github.com/spiffe/spire-plugin-sdk) SDKs for Integration authors @@ -7,19 +10,22 @@ * AWS Support: Support for using [AWS KMS to store signing keys](https://github.com/spiffe/spire/pull/2066), [Support for internet-restricted environments](https://github.com/spiffe/spire/pull/2119) * Support for using [GCP Certificate Authority Service as an upstream authority](https://github.com/spiffe/spire/pull/2172) -**Near-Term and Medium-Term** +## Near-Term and Medium-Term + * Provide a turn-key Kubernetes experience that adheres to security best practices (In Progress) * Provide a privileged API on SPIRE Agent to delegate SVID management to platform integrators (In Progress) * Support for supply chain provenance attestation by verification of binary signing (e.g. TUF/notary/in-toto metadata validation) * Secretless authentication to Google Compute Platform by expanding OIDC Federation integration support -**Long-Term** +## Long-Term + * Key Revocation and Forced Rotation * Ensure error messages are indicative of a direction towards resolution * Improve health-check subsystem * Secretless authentication to Microsoft Azure by expanding OIDC Federation integration support *** - -**Credits** -Thank you to [@anjaltelang](https://github.com/anjaltelang) for helping the SPIRE team keep this roadmap accurate and up-to-date 🎉 \ No newline at end of file + +## Credits + +Thank you to [@anjaltelang](https://github.com/anjaltelang) for helping the SPIRE team keep this roadmap accurate and up-to-date 🎉 diff --git a/SECURITY.md b/SECURITY.md index 54af466422..77fd1c8b05 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,14 +2,7 @@ ## Supported Versions -Versions of the project that are currently being supported with security updates: - -| Version | Supported | -|---------|--------------------| -| 1.2.x | :white_check_mark: | -| 1.1.x | :white_check_mark: | -| <=1.0.x | :x: | - +The project supports security releases for the current minor release series and the previous minor release series, i.e. v1.X and v1.X-1. Example: if the current release series is v1.5, security fixes will be supported for both the v1.4 and v1.5 series. ## Reporting a Vulnerability diff --git a/cmd/spire-agent/cli/api/fetch_jwt.go b/cmd/spire-agent/cli/api/fetch_jwt.go index 8bd9b3bbdb..7e7654f9c6 100644 --- a/cmd/spire-agent/cli/api/fetch_jwt.go +++ b/cmd/spire-agent/cli/api/fetch_jwt.go @@ -8,20 +8,20 @@ import ( "github.com/mitchellh/cli" "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" "github.com/spiffe/spire/pkg/common/cliprinter" ) func NewFetchJWTCommand() cli.Command { - return newFetchJWTCommand(common_cli.DefaultEnv, newWorkloadClient) + return newFetchJWTCommand(commoncli.DefaultEnv, newWorkloadClient) } -func newFetchJWTCommand(env *common_cli.Env, clientMaker workloadClientMaker) cli.Command { +func newFetchJWTCommand(env *commoncli.Env, clientMaker workloadClientMaker) cli.Command { return adaptCommand(env, clientMaker, new(fetchJWTCommand)) } type fetchJWTCommand struct { - audience common_cli.CommaStringsFlag + audience commoncli.CommaStringsFlag spiffeID string printer cliprinter.Printer } @@ -34,7 +34,7 @@ func (c *fetchJWTCommand) synopsis() string { return "Fetches a JWT SVID from the Workload API" } -func (c *fetchJWTCommand) run(ctx context.Context, env *common_cli.Env, client *workloadClient) error { +func (c *fetchJWTCommand) run(ctx context.Context, env *commoncli.Env, client *workloadClient) error { if len(c.audience) == 0 { return errors.New("audience must be specified") } @@ -48,15 +48,14 @@ func (c *fetchJWTCommand) run(ctx context.Context, env *common_cli.Env, client * return err } - c.printer.MustPrintProto(svidResp, bundlesResp) - return nil + return c.printer.PrintProto(svidResp, bundlesResp) } func (c *fetchJWTCommand) appendFlags(fs *flag.FlagSet) { fs.Var(&c.audience, "audience", "comma separated list of audience values") fs.StringVar(&c.spiffeID, "spiffeID", "", "SPIFFE ID subject (optional)") - - cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, printPrettyResult) + outputValue := cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, nil, printPrettyResult) + fs.Var(outputValue, "format", "deprecated; use -output") } func (c *fetchJWTCommand) fetchJWTSVID(ctx context.Context, client *workloadClient) (*workload.JWTSVIDResponse, error) { @@ -78,7 +77,7 @@ func (c *fetchJWTCommand) fetchJWTBundles(ctx context.Context, client *workloadC return stream.Recv() } -func printPrettyResult(results ...interface{}) error { +func printPrettyResult(_ *commoncli.Env, results ...interface{}) error { errMsg := "internal error: cli printer; please report this bug" svidResp, ok := results[0].(*workload.JWTSVIDResponse) diff --git a/cmd/spire-agent/cli/api/fetch_x509.go b/cmd/spire-agent/cli/api/fetch_x509.go index 58eb6add1b..c1188a6d67 100644 --- a/cmd/spire-agent/cli/api/fetch_x509.go +++ b/cmd/spire-agent/cli/api/fetch_x509.go @@ -8,7 +8,6 @@ import ( "errors" "flag" "fmt" - "os" "path" "time" @@ -18,6 +17,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" common_cli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/diskutil" ) func NewFetchX509Command() cli.Command { @@ -153,12 +153,12 @@ func (c *fetchX509Command) writeKey(filename string, privateKey crypto.PrivateKe Bytes: data, } - return os.WriteFile(filename, pem.EncodeToMemory(b), 0600) + return diskutil.WritePrivateFile(filename, pem.EncodeToMemory(b)) } // writeFile creates or truncates filename, and writes data to it func (c *fetchX509Command) writeFile(filename string, data []byte) error { - return os.WriteFile(filename, data, 0644) // nolint: gosec // expected permission for certificates + return diskutil.WritePubliclyReadableFile(filename, data) } type X509SVID struct { diff --git a/cmd/spire-agent/cli/cli.go b/cmd/spire-agent/cli/cli.go index bc5203c079..80fe546887 100644 --- a/cmd/spire-agent/cli/cli.go +++ b/cmd/spire-agent/cli/cli.go @@ -1,6 +1,7 @@ package cli import ( + "context" stdlog "log" "github.com/mitchellh/cli" @@ -17,7 +18,7 @@ type CLI struct { AllowUnknownConfig bool } -func (cc *CLI) Run(args []string) int { +func (cc *CLI) Run(ctx context.Context, args []string) int { c := cli.NewCLI("spire-agent", version.Version()) c.Args = args c.Commands = map[string]cli.CommandFactory{ @@ -37,7 +38,7 @@ func (cc *CLI) Run(args []string) int { return &api.WatchCLI{}, nil }, "run": func() (cli.Command, error) { - return run.NewRunCommand(cc.LogOptions, cc.AllowUnknownConfig), nil + return run.NewRunCommand(ctx, cc.LogOptions, cc.AllowUnknownConfig), nil }, "healthcheck": func() (cli.Command, error) { return healthcheck.NewHealthCheckCommand(), nil diff --git a/cmd/spire-agent/cli/run/run.go b/cmd/spire-agent/cli/run/run.go index 92d69efbca..4d55ba9993 100644 --- a/cmd/spire-agent/cli/run/run.go +++ b/cmd/spire-agent/cli/run/run.go @@ -111,17 +111,19 @@ type experimentalConfig struct { } type Command struct { + ctx context.Context logOptions []log.Option env *common_cli.Env allowUnknownConfig bool } -func NewRunCommand(logOptions []log.Option, allowUnknownConfig bool) cli.Command { - return newRunCommand(common_cli.DefaultEnv, logOptions, allowUnknownConfig) +func NewRunCommand(ctx context.Context, logOptions []log.Option, allowUnknownConfig bool) cli.Command { + return newRunCommand(ctx, common_cli.DefaultEnv, logOptions, allowUnknownConfig) } -func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command { +func newRunCommand(ctx context.Context, env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command { return &Command{ + ctx: ctx, env: env, logOptions: logOptions, allowUnknownConfig: allowUnknownConfig, @@ -183,7 +185,11 @@ func (cmd *Command) Run(args []string) int { a := agent.New(c) - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + ctx := cmd.ctx + if ctx == nil { + ctx = context.Background() + } + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer stop() err = a.Run(ctx) diff --git a/cmd/spire-agent/cli/run/run_posix_test.go b/cmd/spire-agent/cli/run/run_posix_test.go index 34e459f8dd..761822c134 100644 --- a/cmd/spire-agent/cli/run/run_posix_test.go +++ b/cmd/spire-agent/cli/run/run_posix_test.go @@ -5,15 +5,145 @@ package run import ( "bytes" + "fmt" "os" + "syscall" "testing" "github.com/hashicorp/hcl/hcl/printer" "github.com/spiffe/spire/pkg/agent" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/fflag" + "github.com/spiffe/spire/pkg/common/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestCommand_Run(t *testing.T) { + testTempDir := t.TempDir() + testDataDir := fmt.Sprintf("%s/data", testTempDir) + testAgentSocketDir := fmt.Sprintf("%s/spire-agent", testTempDir) + + type fields struct { + logOptions []log.Option + env *commoncli.Env + allowUnknownConfig bool + } + type args struct { + args []string + } + type want struct { + code int + dataDirCreated bool + agentUdsDirCreated bool + stderrContent string + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "don't create any dir when error loading nonexistent config", + args: args{ + args: []string{}, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + agentUdsDirCreated: false, + dataDirCreated: false, + stderrContent: "could not find config file", + }, + }, + { + name: "don't create any dir when error loading invalid config", + args: args{ + args: []string{ + "-config", "../../../../test/fixture/config/agent_run_posix.conf", + "-namedPipeName", "\\spire-agent\\public\\api", + }, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + agentUdsDirCreated: false, + dataDirCreated: false, + stderrContent: "flag provided but not defined: -namedPipeName", + }, + }, + { + name: "creates spire-agent uds and data dirs", + args: args{ + args: []string{ + "-config", "../../../../test/fixture/config/agent_run_posix.conf", + "-trustBundle", "../../../../conf/agent/dummy_root_ca.crt", + "-dataDir", testDataDir, + "-socketPath", fmt.Sprintf("%s/spire-agent/api.sock", testTempDir), + }, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + agentUdsDirCreated: true, + dataDirCreated: true, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + _ = fflag.Unload() + os.RemoveAll(testDataDir) + + cmd := &Command{ + logOptions: testCase.fields.logOptions, + env: testCase.fields.env, + allowUnknownConfig: testCase.fields.allowUnknownConfig, + } + + code := cmd.Run(testCase.args.args) + + assert.Equal(t, testCase.want.code, code) + if testCase.want.stderrContent == "" { + assert.Empty(t, testCase.fields.env.Stderr.(*bytes.Buffer).String()) + } else { + assert.Contains(t, testCase.fields.env.Stderr.(*bytes.Buffer).String(), testCase.want.stderrContent) + } + if testCase.want.agentUdsDirCreated { + assert.DirExistsf(t, testAgentSocketDir, "spire-agent uds dir should be created") + currentUmask := syscall.Umask(0) + assert.Equalf(t, currentUmask, 0027, "spire-agent process should be created with 0027 umask") + } else { + assert.NoDirExistsf(t, testAgentSocketDir, "spire-agent uds dir should not be created") + } + if testCase.want.dataDirCreated { + assert.DirExistsf(t, testDataDir, "expected data directory to be created") + } else { + assert.NoDirExistsf(t, testDataDir, "expected data directory to not be created") + } + }) + } +} + func TestParseFlagsGood(t *testing.T) { c, err := parseFlags("run", []string{ "-dataDir=.", @@ -163,7 +293,7 @@ func newAgentConfigCasesOS() []newAgentConfigCase { }, }, { - msg: "admin_socket_path configured with similar folther that socket_path", + msg: "admin_socket_path configured with similar folder that socket_path", input: func(c *Config) { c.Agent.SocketPath = "/tmp/workload/workload.sock" c.Agent.AdminSocketPath = "/tmp/workload-admin/admin.sock" diff --git a/cmd/spire-agent/cli/run/run_windows_test.go b/cmd/spire-agent/cli/run/run_windows_test.go index 1229789c28..79401ada5e 100644 --- a/cmd/spire-agent/cli/run/run_windows_test.go +++ b/cmd/spire-agent/cli/run/run_windows_test.go @@ -5,16 +5,132 @@ package run import ( "bytes" + "fmt" "os" "testing" "github.com/hashicorp/hcl/hcl/printer" "github.com/spiffe/spire/pkg/agent" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/fflag" + "github.com/spiffe/spire/pkg/common/log" "github.com/spiffe/spire/pkg/common/namedpipe" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestCommand_Run(t *testing.T) { + testTempDir := t.TempDir() + testDataDir := fmt.Sprintf("%s/data", testTempDir) + + type fields struct { + logOptions []log.Option + env *commoncli.Env + allowUnknownConfig bool + } + type args struct { + args []string + } + type want struct { + code int + stderrContent string + dataDirCreated bool + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "don't create any dir when error loading nonexistent config", + args: args{ + args: []string{}, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + dataDirCreated: false, + stderrContent: "could not find config file", + }, + }, + { + name: "don't create any dir when error loading invalid config", + args: args{ + args: []string{ + "-config", "../../../../test/fixture/config/agent_run_windows.conf", + "-socketPath", "unix:///tmp/agent.sock", + }, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + dataDirCreated: false, + stderrContent: "flag provided but not defined: -socketPath", + }, + }, + { + name: "create data dir and uses named pipe", + args: args{ + args: []string{ + "-config", "../../../../test/fixture/config/agent_run_windows.conf", + "-dataDir", testDataDir, + "-namedPipeName", "\\spire-agent\\public\\api", + }, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + dataDirCreated: true, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + _ = fflag.Unload() + os.RemoveAll(testTempDir) + + cmd := &Command{ + logOptions: testCase.fields.logOptions, + env: testCase.fields.env, + allowUnknownConfig: testCase.fields.allowUnknownConfig, + } + + result := cmd.Run(testCase.args.args) + + assert.Equal(t, testCase.want.code, result) + if testCase.want.stderrContent == "" { + assert.Empty(t, testCase.fields.env.Stderr.(*bytes.Buffer).String()) + } else { + assert.Contains(t, testCase.fields.env.Stderr.(*bytes.Buffer).String(), testCase.want.stderrContent) + } + if testCase.want.dataDirCreated { + assert.DirExistsf(t, testDataDir, "expected data directory to be created") + } else { + assert.NoDirExistsf(t, testDataDir, "expected data directory to not be created") + } + }) + } +} + func TestParseFlagsGood(t *testing.T) { c, err := parseFlags("run", []string{ "-dataDir=.", diff --git a/cmd/spire-agent/main.go b/cmd/spire-agent/main.go index 10ac96733f..c208ddeb22 100644 --- a/cmd/spire-agent/main.go +++ b/cmd/spire-agent/main.go @@ -4,8 +4,9 @@ import ( "os" "github.com/spiffe/spire/cmd/spire-agent/cli" + "github.com/spiffe/spire/pkg/common/entrypoint" ) func main() { - os.Exit(new(cli.CLI).Run(os.Args[1:])) + os.Exit(entrypoint.NewEntryPoint(new(cli.CLI).Run).Main()) } diff --git a/cmd/spire-server/cli/agent/agent_posix_test.go b/cmd/spire-server/cli/agent/agent_posix_test.go index 5cda6991d0..c80fdb0081 100644 --- a/cmd/spire-server/cli/agent/agent_posix_test.go +++ b/cmd/spire-server/cli/agent/agent_posix_test.go @@ -7,9 +7,41 @@ var ( listUsage = `Usage of agent list: -matchSelectorsOn string The match mode used when filtering by selectors. Options: exact, any, superset and subset (default "superset") + -output value + Desired output format (pretty, json); default: pretty. -selector value A colon-delimited type:value selector. Can be used more than once -socketPath string Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + banUsage = `Usage of agent ban: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") + -spiffeID string + The SPIFFE ID of the agent to ban (agent identity) +` + evictUsage = `Usage of agent evict: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") + -spiffeID string + The SPIFFE ID of the agent to evict (agent identity) +` + countUsage = `Usage of agent count: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + showUsage = `Usage of agent show: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") + -spiffeID string + The SPIFFE ID of the agent to show (agent identity) ` ) diff --git a/cmd/spire-server/cli/agent/agent_test.go b/cmd/spire-server/cli/agent/agent_test.go index de9dbd27c2..f491c3500c 100644 --- a/cmd/spire-server/cli/agent/agent_test.go +++ b/cmd/spire-server/cli/agent/agent_test.go @@ -3,20 +3,20 @@ package agent_test import ( "bytes" "context" + "fmt" "testing" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "github.com/mitchellh/cli" agentv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/cli/agent" "github.com/spiffe/spire/cmd/spire-server/cli/common" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" "github.com/spiffe/spire/test/spiretest" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" ) @@ -40,16 +40,15 @@ var ( }, }, } + availableFormats = []string{"pretty", "json"} ) type agentTest struct { stdin *bytes.Buffer stdout *bytes.Buffer stderr *bytes.Buffer - args []string server *fakeAgentServer - client cli.Command } @@ -64,26 +63,25 @@ func TestBanHelp(t *testing.T) { test := setupTest(t, agent.NewBanCommandWithEnv) test.client.Help() - require.Equal(t, `Usage of agent ban:`+common.AddrUsage+ - ` -spiffeID string - The SPIFFE ID of the agent to ban (agent identity) -`, test.stderr.String()) + require.Equal(t, banUsage, test.stderr.String()) } func TestBan(t *testing.T) { for _, tt := range []struct { - name string - args []string - expectReturnCode int - expectStdout string - expectStderr string - serverErr error + name string + args []string + expectReturnCode int + expectStdoutPretty string + expectStdoutJSON string + expectStderr string + serverErr error }{ { - name: "success", - args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent1"}, - expectReturnCode: 0, - expectStdout: "Agent banned successfully\n", + name: "success", + args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent1"}, + expectReturnCode: 0, + expectStdoutPretty: "Agent banned successfully\n", + expectStdoutJSON: "{}", }, { name: "no spiffe id", @@ -104,16 +102,20 @@ func TestBan(t *testing.T) { expectStderr: "Error: rpc error: code = Internal desc = internal server error\n", }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, agent.NewBanCommandWithEnv) - test.server.err = tt.serverErr - - returnCode := test.client.Run(append(test.args, tt.args...)) - require.Equal(t, tt.expectStdout, test.stdout.String()) - require.Equal(t, tt.expectStderr, test.stderr.String()) - require.Equal(t, tt.expectReturnCode, returnCode) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, agent.NewBanCommandWithEnv) + test.server.err = tt.serverErr + args := tt.args + args = append(args, "-output", format) + + returnCode := test.client.Run(append(test.args, args...)) + + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectStdoutPretty, tt.expectStdoutJSON) + require.Equal(t, tt.expectStderr, test.stderr.String()) + require.Equal(t, tt.expectReturnCode, returnCode) + }) + } } } @@ -121,26 +123,25 @@ func TestEvictHelp(t *testing.T) { test := setupTest(t, agent.NewEvictCommandWithEnv) test.client.Help() - require.Equal(t, `Usage of agent evict:`+common.AddrUsage+ - ` -spiffeID string - The SPIFFE ID of the agent to evict (agent identity) -`, test.stderr.String()) + require.Equal(t, evictUsage, test.stderr.String()) } func TestEvict(t *testing.T) { for _, tt := range []struct { - name string - args []string - expectedReturnCode int - expectedStdout string - expectedStderr string - serverErr error + name string + args []string + expectedReturnCode int + expectedStdoutPretty string + expectedStdoutJSON string + expectedStderr string + serverErr error }{ { - name: "success", - args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent1"}, - expectedReturnCode: 0, - expectedStdout: "Agent evicted successfully\n", + name: "success", + args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent1"}, + expectedReturnCode: 0, + expectedStdoutPretty: "Agent evicted successfully\n", + expectedStdoutJSON: "{}", }, { name: "no spiffe id", @@ -161,16 +162,20 @@ func TestEvict(t *testing.T) { expectedStderr: "Error: rpc error: code = Internal desc = internal server error\n", }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, agent.NewEvictCommandWithEnv) - test.server.err = tt.serverErr - - returnCode := test.client.Run(append(test.args, tt.args...)) - require.Equal(t, tt.expectedStdout, test.stdout.String()) - require.Equal(t, tt.expectedStderr, test.stderr.String()) - require.Equal(t, tt.expectedReturnCode, returnCode) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, agent.NewEvictCommandWithEnv) + test.server.err = tt.serverErr + args := tt.args + args = append(args, "-output", format) + + returnCode := test.client.Run(append(test.args, args...)) + + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Equal(t, tt.expectedStderr, test.stderr.String()) + require.Equal(t, tt.expectedReturnCode, returnCode) + }) + } } } @@ -178,29 +183,32 @@ func TestCountHelp(t *testing.T) { test := setupTest(t, agent.NewCountCommandWithEnv) test.client.Help() - require.Equal(t, `Usage of agent count:`+common.AddrUsage, test.stderr.String()) + require.Equal(t, countUsage, test.stderr.String()) } func TestCount(t *testing.T) { for _, tt := range []struct { - name string - args []string - expectedReturnCode int - expectedStdout string - expectedStderr string - existentAgents []*types.Agent - serverErr error + name string + args []string + expectedReturnCode int + expectedStdoutPretty string + expectedStdoutJSON string + expectedStderr string + existentAgents []*types.Agent + serverErr error }{ { - name: "0 agents", - expectedReturnCode: 0, - expectedStdout: "0 attested agents", + name: "0 agents", + expectedReturnCode: 0, + expectedStdoutPretty: "0 attested agents", + expectedStdoutJSON: `{"count":0}`, }, { - name: "count 1 agent", - expectedReturnCode: 0, - expectedStdout: "1 attested agent", - existentAgents: testAgents, + name: "count 1 agent", + expectedReturnCode: 0, + expectedStdoutPretty: "1 attested agent", + expectedStdoutJSON: `{"count":1}`, + existentAgents: testAgents, }, { name: "server error", @@ -215,16 +223,21 @@ func TestCount(t *testing.T) { expectedStderr: common.AddrError, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, agent.NewCountCommandWithEnv) - test.server.agents = tt.existentAgents - test.server.err = tt.serverErr - returnCode := test.client.Run(append(test.args, tt.args...)) - require.Contains(t, test.stdout.String(), tt.expectedStdout) - require.Equal(t, tt.expectedStderr, test.stderr.String()) - require.Equal(t, tt.expectedReturnCode, returnCode) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, agent.NewCountCommandWithEnv) + test.server.agents = tt.existentAgents + test.server.err = tt.serverErr + args := tt.args + args = append(args, "-output", format) + + returnCode := test.client.Run(append(test.args, args...)) + + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Equal(t, tt.expectedStderr, test.stderr.String()) + require.Equal(t, tt.expectedReturnCode, returnCode) + }) + } } } @@ -237,20 +250,23 @@ func TestListHelp(t *testing.T) { func TestList(t *testing.T) { for _, tt := range []struct { - name string - args []string - expectedReturnCode int - expectedStdout string - expectedStderr string - expectReq *agentv1.ListAgentsRequest - existentAgents []*types.Agent - serverErr error + name string + args []string + expectedReturnCode int + expectedStdoutPretty string + expectedStdoutJSON string + expectedStderr string + expectReq *agentv1.ListAgentsRequest + existentAgents []*types.Agent + expectedFormat string + serverErr error }{ { - name: "1 agent", - expectedReturnCode: 0, - existentAgents: testAgents, - expectedStdout: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + name: "1 agent", + expectedReturnCode: 0, + existentAgents: testAgents, + expectedStdoutPretty: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + expectedStdoutJSON: `{"agents":[{"id":{"trust_domain":"example.org","path":"/spire/agent/agent1"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":false}],"next_page_token":""}`, expectReq: &agentv1.ListAgentsRequest{ Filter: &agentv1.ListAgentsRequest_Filter{}, PageSize: 1000, @@ -259,6 +275,7 @@ func TestList(t *testing.T) { { name: "no agents", expectedReturnCode: 0, + expectedStdoutJSON: `{"agents":[],"next_page_token":""}`, expectReq: &agentv1.ListAgentsRequest{ Filter: &agentv1.ListAgentsRequest_Filter{}, PageSize: 1000, @@ -289,8 +306,9 @@ func TestList(t *testing.T) { }, PageSize: 1000, }, - existentAgents: testAgents, - expectedStdout: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + existentAgents: testAgents, + expectedStdoutPretty: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + expectedStdoutJSON: `{"agents":[{"id":{"trust_domain":"example.org","path":"/spire/agent/agent1"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":false}],"next_page_token":""}`, }, { name: "by selector: any matcher", @@ -307,8 +325,9 @@ func TestList(t *testing.T) { }, PageSize: 1000, }, - existentAgents: testAgents, - expectedStdout: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + existentAgents: testAgents, + expectedStdoutPretty: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + expectedStdoutJSON: `{"agents":[{"id":{"trust_domain":"example.org","path":"/spire/agent/agent1"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":false}],"next_page_token":""}`, }, { name: "by selector: exact matcher", @@ -325,8 +344,9 @@ func TestList(t *testing.T) { }, PageSize: 1000, }, - existentAgents: testAgents, - expectedStdout: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + existentAgents: testAgents, + expectedStdoutPretty: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + expectedStdoutJSON: `{"agents":[{"id":{"trust_domain":"example.org","path":"/spire/agent/agent1"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":false}],"next_page_token":""}`, }, { name: "by selector: superset matcher", @@ -343,8 +363,9 @@ func TestList(t *testing.T) { }, PageSize: 1000, }, - existentAgents: testAgents, - expectedStdout: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + existentAgents: testAgents, + expectedStdoutPretty: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + expectedStdoutJSON: `{"agents":[{"id":{"trust_domain":"example.org","path":"/spire/agent/agent1"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":false}],"next_page_token":""}`, }, { name: "by selector: subset matcher", @@ -361,8 +382,9 @@ func TestList(t *testing.T) { }, PageSize: 1000, }, - existentAgents: testAgents, - expectedStdout: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + existentAgents: testAgents, + expectedStdoutPretty: "Found 1 attested agent:\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + expectedStdoutJSON: `{"agents":[{"id":{"trust_domain":"example.org","path":"/spire/agent/agent1"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":false}],"next_page_token":""}`, }, { name: "List by selectors: Invalid matcher", @@ -383,18 +405,22 @@ func TestList(t *testing.T) { expectedStderr: common.AddrError, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, agent.NewListCommandWithEnv) - test.server.agents = tt.existentAgents - test.server.err = tt.serverErr - returnCode := test.client.Run(append(test.args, tt.args...)) - - spiretest.RequireProtoEqual(t, tt.expectReq, test.server.gotListAgentRequest) - require.Contains(t, test.stdout.String(), tt.expectedStdout) - require.Equal(t, tt.expectedStderr, test.stderr.String()) - require.Equal(t, tt.expectedReturnCode, returnCode) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, agent.NewListCommandWithEnv) + test.server.agents = tt.existentAgents + test.server.err = tt.serverErr + args := tt.args + args = append(args, "-output", format) + + returnCode := test.client.Run(append(test.args, args...)) + + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + spiretest.RequireProtoEqual(t, tt.expectReq, test.server.gotListAgentRequest) + require.Equal(t, tt.expectedStderr, test.stderr.String()) + require.Equal(t, tt.expectedReturnCode, returnCode) + }) + } } } @@ -402,28 +428,27 @@ func TestShowHelp(t *testing.T) { test := setupTest(t, agent.NewShowCommandWithEnv) test.client.Help() - require.Equal(t, `Usage of agent show:`+common.AddrUsage+ - ` -spiffeID string - The SPIFFE ID of the agent to show (agent identity) -`, test.stderr.String()) + require.Equal(t, showUsage, test.stderr.String()) } func TestShow(t *testing.T) { for _, tt := range []struct { - name string - args []string - expectedReturnCode int - expectedStdout string - expectedStderr string - existentAgents []*types.Agent - serverErr error + name string + args []string + expectedReturnCode int + expectedStdoutPretty string + expectedStdoutJSON string + expectedStderr string + existentAgents []*types.Agent + serverErr error }{ { - name: "success", - args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent1"}, - expectedReturnCode: 0, - existentAgents: testAgents, - expectedStdout: "Found an attested agent given its SPIFFE ID\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + name: "success", + args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent1"}, + expectedReturnCode: 0, + existentAgents: testAgents, + expectedStdoutPretty: "Found an attested agent given its SPIFFE ID\n\nSPIFFE ID : spiffe://example.org/spire/agent/agent1", + expectedStdoutJSON: `{"id":{"trust_domain":"example.org","path":"/spire/agent/agent1"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":false}`, }, { name: "no spiffe id", @@ -445,35 +470,41 @@ func TestShow(t *testing.T) { expectedStderr: common.AddrError, }, { - name: "show selectors", - args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent2"}, - existentAgents: testAgentsWithSelectors, - expectedReturnCode: 0, - expectedStdout: "Selectors : k8s_psat:agent_ns:spire\nSelectors : k8s_psat:agent_sa:spire-agent\nSelectors : k8s_psat:cluster:demo-cluster", + name: "show selectors", + args: []string{"-spiffeID", "spiffe://example.org/spire/agent/agent2"}, + existentAgents: testAgentsWithSelectors, + expectedReturnCode: 0, + expectedStdoutPretty: "Selectors : k8s_psat:agent_ns:spire\nSelectors : k8s_psat:agent_sa:spire-agent\nSelectors : k8s_psat:cluster:demo-cluster", + expectedStdoutJSON: `{"id":{"trust_domain":"example.org","path":"/spire/agent/agent2"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[{"type":"k8s_psat","value":"agent_ns:spire"},{"type":"k8s_psat","value":"agent_sa:spire-agent"},{"type":"k8s_psat","value":"cluster:demo-cluster"}],"banned":false}`, }, { - name: "show banned", - args: []string{"-spiffeID", "spiffe://example.org/spire/agent/banned"}, - existentAgents: testAgentsWithBanned, - expectedReturnCode: 0, - expectedStdout: "Banned : true", + name: "show banned", + args: []string{"-spiffeID", "spiffe://example.org/spire/agent/banned"}, + existentAgents: testAgentsWithBanned, + expectedReturnCode: 0, + expectedStdoutPretty: "Banned : true", + expectedStdoutJSON: `{"id":{"trust_domain":"example.org","path":"/spire/agent/banned"},"attestation_type":"","x509svid_serial_number":"","x509svid_expires_at":"0","selectors":[],"banned":true}`, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, agent.NewShowCommandWithEnv) - test.server.err = tt.serverErr - test.server.agents = tt.existentAgents - - returnCode := test.client.Run(append(test.args, tt.args...)) - require.Contains(t, test.stdout.String(), tt.expectedStdout) - require.Equal(t, tt.expectedStderr, test.stderr.String()) - require.Equal(t, tt.expectedReturnCode, returnCode) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, agent.NewShowCommandWithEnv) + test.server.err = tt.serverErr + test.server.agents = tt.existentAgents + args := tt.args + args = append(args, "-output", format) + + returnCode := test.client.Run(append(test.args, args...)) + + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Equal(t, tt.expectedStderr, test.stderr.String()) + require.Equal(t, tt.expectedReturnCode, returnCode) + }) + } } } -func setupTest(t *testing.T, newClient func(*common_cli.Env) cli.Command) *agentTest { +func setupTest(t *testing.T, newClient func(*commoncli.Env) cli.Command) *agentTest { server := &fakeAgentServer{} addr := spiretest.StartGRPCServer(t, func(s *grpc.Server) { @@ -484,7 +515,7 @@ func setupTest(t *testing.T, newClient func(*common_cli.Env) cli.Command) *agent stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - client := newClient(&common_cli.Env{ + client := newClient(&commoncli.Env{ Stdin: stdin, Stdout: stdout, Stderr: stderr, @@ -542,3 +573,16 @@ func (s *fakeAgentServer) GetAgent(ctx context.Context, req *agentv1.GetAgentReq return nil, s.err } + +func requireOutputBasedOnFormat(t *testing.T, format, stdoutString string, expectedStdoutPretty, expectedStdoutJSON string) { + switch format { + case "pretty": + require.Contains(t, stdoutString, expectedStdoutPretty) + case "json": + if expectedStdoutJSON != "" { + require.JSONEq(t, expectedStdoutJSON, stdoutString) + } else { + require.Empty(t, stdoutString) + } + } +} diff --git a/cmd/spire-server/cli/agent/agent_windows_test.go b/cmd/spire-server/cli/agent/agent_windows_test.go index 07e0561819..4b63d02443 100644 --- a/cmd/spire-server/cli/agent/agent_windows_test.go +++ b/cmd/spire-server/cli/agent/agent_windows_test.go @@ -9,7 +9,39 @@ var ( The match mode used when filtering by selectors. Options: exact, any, superset and subset (default "superset") -namedPipeName string Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. -selector value A colon-delimited type:value selector. Can be used more than once +` + banUsage = `Usage of agent ban: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. + -spiffeID string + The SPIFFE ID of the agent to ban (agent identity) +` + evictUsage = `Usage of agent evict: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. + -spiffeID string + The SPIFFE ID of the agent to evict (agent identity) +` + countUsage = `Usage of agent count: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + showUsage = `Usage of agent show: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. + -spiffeID string + The SPIFFE ID of the agent to show (agent identity) ` ) diff --git a/cmd/spire-server/cli/agent/ban.go b/cmd/spire-server/cli/agent/ban.go index 6fc8a08b40..43fa6a22cb 100644 --- a/cmd/spire-server/cli/agent/ban.go +++ b/cmd/spire-server/cli/agent/ban.go @@ -9,24 +9,27 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" agentv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "github.com/spiffe/spire/pkg/server/api" ) type banCommand struct { + env *commoncli.Env // SPIFFE ID of agent being banned spiffeID string + printer cliprinter.Printer } // NewBanCommand creates a new "ban" subcommand for "agent" command. func NewBanCommand() cli.Command { - return NewBanCommandWithEnv(common_cli.DefaultEnv) + return NewBanCommandWithEnv(commoncli.DefaultEnv) } // NewBanCommandWithEnv creates a new "ban" subcommand for "agent" command // using the environment specified -func NewBanCommandWithEnv(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(banCommand)) +func NewBanCommandWithEnv(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &banCommand{env: env}) } func (*banCommand) Name() string { @@ -38,7 +41,7 @@ func (*banCommand) Synopsis() string { } // Run ban an agent given its SPIFFE ID -func (c *banCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *banCommand) Run(ctx context.Context, _ *commoncli.Env, serverClient util.ServerClient) error { if c.spiffeID == "" { return errors.New("a SPIFFE ID is required") } @@ -49,15 +52,22 @@ func (c *banCommand) Run(ctx context.Context, env *common_cli.Env, serverClient } agentClient := serverClient.NewAgentClient() - if _, err := agentClient.BanAgent(ctx, &agentv1.BanAgentRequest{ + banResponse, err := agentClient.BanAgent(ctx, &agentv1.BanAgentRequest{ Id: api.ProtoFromID(id), - }); err != nil { + }) + if err != nil { return err } - return env.Println("Agent banned successfully") + return c.printer.PrintProto(banResponse) } func (c *banCommand) AppendFlags(fs *flag.FlagSet) { fs.StringVar(&c.spiffeID, "spiffeID", "", "The SPIFFE ID of the agent to ban (agent identity)") + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintBanResult) +} + +func prettyPrintBanResult(env *commoncli.Env, _ ...interface{}) error { + env.Println("Agent banned successfully") + return nil } diff --git a/cmd/spire-server/cli/agent/count.go b/cmd/spire-server/cli/agent/count.go index d4ac843f6d..f55000544d 100644 --- a/cmd/spire-server/cli/agent/count.go +++ b/cmd/spire-server/cli/agent/count.go @@ -1,54 +1,65 @@ package agent import ( + "errors" "flag" "fmt" "github.com/mitchellh/cli" - agentv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" - + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "golang.org/x/net/context" ) -type countCommand struct{} +type countCommand struct { + env *commoncli.Env + printer cliprinter.Printer +} // NewCountCommand creates a new "count" subcommand for "agent" command. func NewCountCommand() cli.Command { - return NewCountCommandWithEnv(common_cli.DefaultEnv) + return NewCountCommandWithEnv(commoncli.DefaultEnv) } // NewCountCommandWithEnv creates a new "count" subcommand for "agent" command // using the environment specified. -func NewCountCommandWithEnv(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(countCommand)) +func NewCountCommandWithEnv(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &countCommand{env: env}) } func (*countCommand) Name() string { return "agent count" } -func (countCommand) Synopsis() string { +func (*countCommand) Synopsis() string { return "Count attested agents" } // Run counts attested agents -func (c *countCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *countCommand) Run(ctx context.Context, _ *commoncli.Env, serverClient util.ServerClient) error { agentClient := serverClient.NewAgentClient() countResponse, err := agentClient.CountAgents(ctx, &agentv1.CountAgentsRequest{}) if err != nil { return err } - count := int(countResponse.Count) - msg := fmt.Sprintf("%d attested ", count) - msg = util.Pluralizer(msg, "agent", "agents", count) - _ = env.Println(msg) - - return nil + return c.printer.PrintProto(countResponse) } func (c *countCommand) AppendFlags(fs *flag.FlagSet) { + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintCount) +} + +func prettyPrintCount(env *commoncli.Env, results ...interface{}) error { + countResp, ok := results[0].(*agentv1.CountAgentsResponse) + if !ok { + return errors.New("internal error: cli printer; please report this bug") + } + count := int(countResp.Count) + msg := fmt.Sprintf("%d attested ", count) + msg = util.Pluralizer(msg, "agent", "agents", count) + env.Println(msg) + return nil } diff --git a/cmd/spire-server/cli/agent/evict.go b/cmd/spire-server/cli/agent/evict.go index 1de8552a4d..0df60ef764 100644 --- a/cmd/spire-server/cli/agent/evict.go +++ b/cmd/spire-server/cli/agent/evict.go @@ -6,41 +6,42 @@ import ( "github.com/mitchellh/cli" "github.com/spiffe/go-spiffe/v2/spiffeid" - agentv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "github.com/spiffe/spire/pkg/server/api" - "golang.org/x/net/context" ) type evictCommand struct { + env *commoncli.Env // SPIFFE ID of the agent being evicted spiffeID string + printer cliprinter.Printer } // NewEvictCommand creates a new "evict" subcommand for "agent" command. func NewEvictCommand() cli.Command { - return NewEvictCommandWithEnv(common_cli.DefaultEnv) + return NewEvictCommandWithEnv(commoncli.DefaultEnv) } // NewEvictCommandWithEnv creates a new "evict" subcommand for "agent" command // using the environment specified -func NewEvictCommandWithEnv(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(evictCommand)) +func NewEvictCommandWithEnv(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &evictCommand{env: env}) } func (*evictCommand) Name() string { return "agent evict" } -func (evictCommand) Synopsis() string { +func (*evictCommand) Synopsis() string { return "Evicts an attested agent given its SPIFFE ID" } // Run evicts an agent given its SPIFFE ID -func (c *evictCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *evictCommand) Run(ctx context.Context, _ *commoncli.Env, serverClient util.ServerClient) error { if c.spiffeID == "" { return errors.New("a SPIFFE ID is required") } @@ -51,14 +52,20 @@ func (c *evictCommand) Run(ctx context.Context, env *common_cli.Env, serverClien } agentClient := serverClient.NewAgentClient() - _, err = agentClient.DeleteAgent(ctx, &agentv1.DeleteAgentRequest{Id: api.ProtoFromID(id)}) + delAgentResponse, err := agentClient.DeleteAgent(ctx, &agentv1.DeleteAgentRequest{Id: api.ProtoFromID(id)}) if err != nil { return err } - return env.Println("Agent evicted successfully") + return c.printer.PrintProto(delAgentResponse) } func (c *evictCommand) AppendFlags(fs *flag.FlagSet) { fs.StringVar(&c.spiffeID, "spiffeID", "", "The SPIFFE ID of the agent to evict (agent identity)") + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintEvictResult) +} + +func prettyPrintEvictResult(env *commoncli.Env, _ ...interface{}) error { + env.Println("Agent evicted successfully") + return nil } diff --git a/cmd/spire-server/cli/agent/list.go b/cmd/spire-server/cli/agent/list.go index 957d06d98a..cab154de8c 100644 --- a/cmd/spire-server/cli/agent/list.go +++ b/cmd/spire-server/cli/agent/list.go @@ -7,46 +7,46 @@ import ( "time" "github.com/mitchellh/cli" - agentv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "github.com/spiffe/spire/pkg/common/idutil" - "golang.org/x/net/context" ) type listCommand struct { + env *commoncli.Env // Type and value are delimited by a colon (:) // ex. "unix:uid:1000" or "spiffe_id:spiffe://example.org/foo" - selectors common_cli.StringsFlag - + selectors commoncli.StringsFlag // Match used when filtering agents by selectors matchSelectorsOn string + printer cliprinter.Printer } // NewListCommand creates a new "list" subcommand for "agent" command. func NewListCommand() cli.Command { - return NewListCommandWithEnv(common_cli.DefaultEnv) + return NewListCommandWithEnv(commoncli.DefaultEnv) } // NewListCommandWithEnv creates a new "list" subcommand for "agent" command // using the environment specified -func NewListCommandWithEnv(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(listCommand)) +func NewListCommandWithEnv(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &listCommand{env: env}) } func (*listCommand) Name() string { return "agent list" } -func (listCommand) Synopsis() string { +func (*listCommand) Synopsis() string { return "Lists attested agents and their SPIFFE IDs" } // Run lists attested agents -func (c *listCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *listCommand) Run(ctx context.Context, _ *commoncli.Env, serverClient util.ServerClient) error { filter := &agentv1.ListAgentsRequest_Filter{} if len(c.selectors) > 0 { matchBehavior, err := parseToSelectorMatch(c.matchSelectorsOn) @@ -71,7 +71,7 @@ func (c *listCommand) Run(ctx context.Context, env *common_cli.Env, serverClient agentClient := serverClient.NewAgentClient() pageToken := "" - var agents []*types.Agent + response := new(agentv1.ListAgentsResponse) for { listResponse, err := agentClient.ListAgents(ctx, &agentv1.ListAgentsRequest{ PageSize: 1000, // comfortably under the (4 MB/theoretical maximum size of 1 agent in MB) @@ -81,29 +81,39 @@ func (c *listCommand) Run(ctx context.Context, env *common_cli.Env, serverClient if err != nil { return err } - agents = append(agents, listResponse.Agents...) + response.Agents = append(response.Agents, listResponse.Agents...) if pageToken = listResponse.NextPageToken; pageToken == "" { break } } + return c.printer.PrintProto(response) +} + +func (c *listCommand) AppendFlags(fs *flag.FlagSet) { + fs.StringVar(&c.matchSelectorsOn, "matchSelectorsOn", "superset", "The match mode used when filtering by selectors. Options: exact, any, superset and subset") + fs.Var(&c.selectors, "selector", "A colon-delimited type:value selector. Can be used more than once") + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintAgents) +} + +func prettyPrintAgents(env *commoncli.Env, results ...interface{}) error { + listResp, ok := results[0].(*agentv1.ListAgentsResponse) + if !ok { + return errors.New("internal error: cli printer; please report this bug") + } + agents := listResp.Agents + if len(agents) == 0 { return env.Printf("No attested agents found\n") } msg := fmt.Sprintf("Found %d attested ", len(agents)) msg = util.Pluralizer(msg, "agent", "agents", len(agents)) - env.Printf(msg + ":\n\n") - + env.Printf("%s:\n\n", msg) return printAgents(env, agents...) } -func (c *listCommand) AppendFlags(fs *flag.FlagSet) { - fs.StringVar(&c.matchSelectorsOn, "matchSelectorsOn", "superset", "The match mode used when filtering by selectors. Options: exact, any, superset and subset") - fs.Var(&c.selectors, "selector", "A colon-delimited type:value selector. Can be used more than once") -} - -func printAgents(env *common_cli.Env, agents ...*types.Agent) error { +func printAgents(env *commoncli.Env, agents ...*types.Agent) error { for _, agent := range agents { id, err := idutil.IDFromProto(agent.Id) if err != nil { diff --git a/cmd/spire-server/cli/agent/show.go b/cmd/spire-server/cli/agent/show.go index 5c8147a35e..9d440482cc 100644 --- a/cmd/spire-server/cli/agent/show.go +++ b/cmd/spire-server/cli/agent/show.go @@ -6,41 +6,43 @@ import ( "github.com/mitchellh/cli" "github.com/spiffe/go-spiffe/v2/spiffeid" - agentv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" + "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "github.com/spiffe/spire/pkg/server/api" - "golang.org/x/net/context" ) type showCommand struct { + env *commoncli.Env // SPIFFE ID of the agent being showed spiffeID string + printer cliprinter.Printer } // NewShowCommand creates a new "show" subcommand for "agent" command. func NewShowCommand() cli.Command { - return NewShowCommandWithEnv(common_cli.DefaultEnv) + return NewShowCommandWithEnv(commoncli.DefaultEnv) } // NewShowCommandWithEnv creates a new "show" subcommand for "agent" command // using the environment specified -func NewShowCommandWithEnv(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(showCommand)) +func NewShowCommandWithEnv(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &showCommand{env: env}) } func (*showCommand) Name() string { return "agent show" } -func (showCommand) Synopsis() string { +func (*showCommand) Synopsis() string { return "Shows the details of an attested agent given its SPIFFE ID" } // Run shows an agent given its SPIFFE ID -func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *showCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if c.spiffeID == "" { return errors.New("a SPIFFE ID is required") } @@ -56,8 +58,21 @@ func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient return err } - env.Printf("Found an attested agent given its SPIFFE ID\n\n") + return c.printer.PrintProto(agent) +} + +func (c *showCommand) AppendFlags(fs *flag.FlagSet) { + fs.StringVar(&c.spiffeID, "spiffeID", "", "The SPIFFE ID of the agent to show (agent identity)") + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintAgent) +} +func prettyPrintAgent(env *commoncli.Env, results ...interface{}) error { + agent, ok := results[0].(*types.Agent) + if !ok { + return errors.New("internal error: cli printer; please report this bug") + } + + env.Printf("Found an attested agent given its SPIFFE ID\n\n") if err := printAgents(env, agent); err != nil { return err } @@ -67,7 +82,3 @@ func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient } return nil } - -func (c *showCommand) AppendFlags(fs *flag.FlagSet) { - fs.StringVar(&c.spiffeID, "spiffeID", "", "The SPIFFE ID of the agent to show (agent identity)") -} diff --git a/cmd/spire-server/cli/bundle/bundle_posix_test.go b/cmd/spire-server/cli/bundle/bundle_posix_test.go index 8500e78766..ab18144c65 100644 --- a/cmd/spire-server/cli/bundle/bundle_posix_test.go +++ b/cmd/spire-server/cli/bundle/bundle_posix_test.go @@ -9,9 +9,45 @@ var ( The format of the bundle data. Either "pem" or "spiffe". (default "pem") -id string SPIFFE ID of the trust domain + -output value + Desired output format (pretty, json); default: pretty. -path string Path to the bundle data -socketPath string Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + countUsage = `Usage of bundle count: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + deleteUsage = `Usage of bundle delete: + -id string + SPIFFE ID of the trust domain + -mode string + Deletion mode: one of restrict, delete, or dissociate (default "restrict") + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + listUsage = `Usage of bundle list: + -format string + The format to list federated bundles (only pretty output format supports this flag). Either "pem" or "spiffe". (default "pem") + -id string + SPIFFE ID of the trust domain + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + showUsage = `Usage of bundle show: + -format string + The format to show the bundle (only pretty output format supports this flag). Either "pem" or "spiffe". (default "pem") + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") ` ) diff --git a/cmd/spire-server/cli/bundle/bundle_test.go b/cmd/spire-server/cli/bundle/bundle_test.go index 885a3c790a..a16a575100 100644 --- a/cmd/spire-server/cli/bundle/bundle_test.go +++ b/cmd/spire-server/cli/bundle/bundle_test.go @@ -10,7 +10,6 @@ import ( bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/spiffe/spire/cmd/spire-server/util" "github.com/spiffe/spire/pkg/common/pemutil" "github.com/spiffe/spire/test/spiretest" @@ -19,13 +18,13 @@ import ( "google.golang.org/grpc/status" ) +var availableFormats = []string{"pretty", "json"} + func TestShowHelp(t *testing.T) { test := setupTest(t, newShowCommand) test.client.Help() - require.Equal(t, `Usage of bundle show: - -format string - The format to show the bundle. Either "pem" or "spiffe". (default "pem")`+common.AddrUsage, test.stderr.String()) + require.Equal(t, showUsage, test.stderr.String()) } func TestShowSynopsis(t *testing.T) { @@ -34,26 +33,41 @@ func TestShowSynopsis(t *testing.T) { } func TestShow(t *testing.T) { + expectedShowResultJSON := `{ + "trust_domain": "spiffe://example.test", + "x509_authorities": [ + { + "asn1": "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyvsCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09Xmakw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylAdZglS5kKnYigmwDh+/U=" + } + ], + "jwt_authorities": [], + "refresh_hint": "60", + "sequence_number": "0" +}` for _, tt := range []struct { - name string - args []string - expectedOut string - serverErr error - expectedError string + name string + args []string + expectedStdoutPretty string + expectedStdoutJSON string + serverErr error + expectedError string }{ { - name: "default", - expectedOut: cert1PEM, + name: "default", + expectedStdoutPretty: cert1PEM, + expectedStdoutJSON: expectedShowResultJSON, }, { - name: "pem", - args: []string{"-format", util.FormatPEM}, - expectedOut: cert1PEM, + name: "pem", + args: []string{"-format", util.FormatPEM}, + expectedStdoutPretty: cert1PEM, + expectedStdoutJSON: expectedShowResultJSON, }, { - name: "spiffe", - args: []string{"-format", util.FormatSPIFFE}, - expectedOut: cert1JWKS, + name: "spiffe", + args: []string{"-format", util.FormatSPIFFE}, + expectedStdoutPretty: cert1JWKS, + expectedStdoutJSON: expectedShowResultJSON, }, { name: "server fails", @@ -61,29 +75,31 @@ func TestShow(t *testing.T) { expectedError: "Error: rpc error: code = Unknown desc = some error\n", }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newShowCommand) - test.server.err = tt.serverErr - test.server.bundles = []*types.Bundle{{ - TrustDomain: "spiffe://example.test", - X509Authorities: []*types.X509Certificate{ - {Asn1: test.cert1.Raw}, + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newShowCommand) + test.server.err = tt.serverErr + test.server.bundles = []*types.Bundle{{ + TrustDomain: "spiffe://example.test", + X509Authorities: []*types.X509Certificate{ + {Asn1: test.cert1.Raw}, + }, + RefreshHint: 60, }, - RefreshHint: 60, - }, - } - - rc := test.client.Run(test.args(tt.args...)) - if tt.expectedError != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expectedError, test.stderr.String()) - return - } - - require.Equal(t, 0, rc) - require.Equal(t, test.stdout.String(), tt.expectedOut) - }) + } + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(args...)) + if tt.expectedError != "" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectedError, test.stderr.String()) + return + } + assertOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Equal(t, 0, rc) + }) + } } } @@ -99,6 +115,23 @@ func TestSetSynopsis(t *testing.T) { } func TestSet(t *testing.T) { + expectedSetResultJSON := `{ + "results": [ + { + "status": { + "code": 0, + "message": "" + }, + "bundle": { + "trust_domain": "spiffe://otherdomain.test", + "x509_authorities": [], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } + } + ] +}` cert1, err := pemutil.ParseCertificate([]byte(cert1PEM)) require.NoError(t, err) @@ -106,54 +139,64 @@ func TestSet(t *testing.T) { require.NoError(t, err) for _, tt := range []struct { - name string - args []string - expectedStderr string - stdin string - fileData string - serverErr error - toSet *types.Bundle - setResponse *bundlev1.BatchSetFederatedBundleResponse + name string + args []string + expectedStderrPretty string + expectedStderrJSON string + expectedStdoutPretty string + expectedStdoutJSON string + stdin string + fileData string + serverErr error + toSet *types.Bundle + setResponse *bundlev1.BatchSetFederatedBundleResponse }{ { - name: "no id", - expectedStderr: "Error: id flag is required\n", + name: "no id", + expectedStderrPretty: "Error: id flag is required\n", + expectedStderrJSON: "Error: id flag is required\n", }, { - name: "invalid trust domain ID", - expectedStderr: "Error: unable to parse bundle data: no PEM blocks\n", - args: []string{"-id", "spiffe://otherdomain.test"}, + name: "invalid trust domain ID", + expectedStderrPretty: "Error: unable to parse bundle data: no PEM blocks\n", + expectedStderrJSON: "Error: unable to parse bundle data: no PEM blocks\n", + args: []string{"-id", "spiffe://otherdomain.test"}, }, { - name: "invalid output format", - stdin: cert1PEM, - args: []string{"-id", "spiffe://otherdomain.test", "-format", "invalidFormat"}, - expectedStderr: "Error: invalid format: \"invalidformat\"\n", + name: "invalid output format", + stdin: cert1PEM, + args: []string{"-id", "spiffe://otherdomain.test", "-format", "invalidFormat"}, + expectedStderrPretty: "Error: invalid format: \"invalidformat\"\n", + expectedStderrJSON: "Error: invalid format: \"invalidformat\"\n", }, { - name: "invalid bundle (pem)", - stdin: "invalid bundle", - args: []string{"-id", "spiffe://otherdomain.test"}, - expectedStderr: "Error: unable to parse bundle data: no PEM blocks\n", + name: "invalid bundle (pem)", + stdin: "invalid bundle", + args: []string{"-id", "spiffe://otherdomain.test"}, + expectedStderrPretty: "Error: unable to parse bundle data: no PEM blocks\n", + expectedStderrJSON: "Error: unable to parse bundle data: no PEM blocks\n", }, { - name: "invalid bundle (spiffe)", - stdin: "invalid bundle", - args: []string{"-id", "spiffe://otherdomain.test", "-format", util.FormatSPIFFE}, - expectedStderr: "Error: unable to parse to spiffe bundle: spiffebundle: unable to parse JWKS: invalid character 'i' looking for beginning of value\n", + name: "invalid bundle (spiffe)", + stdin: "invalid bundle", + args: []string{"-id", "spiffe://otherdomain.test", "-format", util.FormatSPIFFE}, + expectedStderrPretty: "Error: unable to parse to spiffe bundle: spiffebundle: unable to parse JWKS: invalid character 'i' looking for beginning of value\n", + expectedStderrJSON: "Error: unable to parse to spiffe bundle: spiffebundle: unable to parse JWKS: invalid character 'i' looking for beginning of value\n", }, { - name: "server fails", - stdin: cert1PEM, - args: []string{"-id", "spiffe://otherdomain.test"}, - serverErr: status.New(codes.Internal, "some error").Err(), - expectedStderr: "Error: failed to set federated bundle: rpc error: code = Internal desc = some error\n", + name: "server fails", + stdin: cert1PEM, + args: []string{"-id", "spiffe://otherdomain.test"}, + serverErr: status.New(codes.Internal, "some error").Err(), + expectedStderrPretty: "Error: failed to set federated bundle: rpc error: code = Internal desc = some error\n", + expectedStderrJSON: "Error: failed to set federated bundle: rpc error: code = Internal desc = some error\n", }, { - name: "failed to set", - stdin: cert1PEM, - args: []string{"-id", "spiffe://otherdomain.test"}, - expectedStderr: "Error: failed to set federated bundle: failed to set\n", + name: "failed to set", + stdin: cert1PEM, + args: []string{"-id", "spiffe://otherdomain.test"}, + expectedStderrPretty: "Error: failed to set federated bundle: failed to set\n", + expectedStdoutJSON: `{"results":[{"status":{"code":13,"message":"failed to set"},"bundle":null}]}`, toSet: &types.Bundle{ TrustDomain: "spiffe://otherdomain.test", X509Authorities: []*types.X509Certificate{ @@ -192,6 +235,8 @@ func TestSet(t *testing.T) { }, }, }, + expectedStdoutPretty: "bundle set.", + expectedStdoutJSON: expectedSetResultJSON, }, { name: "set bundle (pem)", @@ -215,6 +260,8 @@ func TestSet(t *testing.T) { }, }, }, + expectedStdoutPretty: "bundle set.", + expectedStdoutJSON: expectedSetResultJSON, }, { name: "set bundle (jwks)", @@ -244,11 +291,14 @@ func TestSet(t *testing.T) { }, }, }, + expectedStdoutPretty: "bundle set.", + expectedStdoutJSON: expectedSetResultJSON, }, { - name: "invalid file name", - expectedStderr: fmt.Sprintf("Error: unable to load bundle data: open /not/a/real/path/to/a/bundle: %s\n", spiretest.PathNotFound()), - args: []string{"-id", "spiffe://otherdomain.test", "-path", "/not/a/real/path/to/a/bundle"}, + name: "invalid file name", + expectedStderrPretty: fmt.Sprintf("Error: unable to load bundle data: open /not/a/real/path/to/a/bundle: %s\n", spiretest.PathNotFound()), + expectedStderrJSON: fmt.Sprintf("Error: unable to load bundle data: open /not/a/real/path/to/a/bundle: %s\n", spiretest.PathNotFound()), + args: []string{"-id", "spiffe://otherdomain.test", "-path", "/not/a/real/path/to/a/bundle"}, }, { name: "set from file (default)", @@ -272,6 +322,8 @@ func TestSet(t *testing.T) { }, }, }, + expectedStdoutPretty: "bundle set.", + expectedStdoutJSON: expectedSetResultJSON, }, { name: "set from file (pem)", @@ -295,6 +347,8 @@ func TestSet(t *testing.T) { }, }, }, + expectedStdoutPretty: "bundle set.", + expectedStdoutJSON: expectedSetResultJSON, }, { name: "set from file (jwks)", @@ -324,36 +378,44 @@ func TestSet(t *testing.T) { }, }, }, + expectedStdoutPretty: "bundle set.", + expectedStdoutJSON: expectedSetResultJSON, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newSetCommand) - test.server.expectedSetBundle = tt.toSet - test.server.setResponse = tt.setResponse - test.server.err = tt.serverErr - - test.stdin.WriteString(tt.stdin) - var extraArgs []string - if tt.fileData != "" { - tmpDir := spiretest.TempDir(t) - bundlePath := filepath.Join(tmpDir, "bundle_data") - require.NoError(t, os.WriteFile(bundlePath, []byte(tt.fileData), 0600)) - extraArgs = append(extraArgs, "-path", bundlePath) - } - - rc := test.client.Run(test.args(append(tt.args, extraArgs...)...)) - - if tt.expectedStderr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expectedStderr, test.stderr.String()) - return - } - - require.Empty(t, test.stderr.String()) - require.Equal(t, 0, rc) - require.Equal(t, "bundle set.\n", test.stdout.String()) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newSetCommand) + test.server.expectedSetBundle = tt.toSet + test.server.setResponse = tt.setResponse + test.server.err = tt.serverErr + test.stdin.WriteString(tt.stdin) + var extraArgs []string + if tt.fileData != "" { + tmpDir := spiretest.TempDir(t) + bundlePath := filepath.Join(tmpDir, "bundle_data") + require.NoError(t, os.WriteFile(bundlePath, []byte(tt.fileData), 0600)) + extraArgs = append(extraArgs, "-path", bundlePath) + } + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(append(args, extraArgs...)...)) + + if tt.expectedStderrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectedStderrPretty, test.stderr.String()) + return + } + if tt.expectedStderrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectedStderrJSON, test.stderr.String()) + return + } + assertOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Empty(t, test.stderr.String()) + require.Equal(t, 0, rc) + }) + } } } @@ -361,7 +423,7 @@ func TestCountHelp(t *testing.T) { test := setupTest(t, NewCountCommandWithEnv) test.client.Help() - require.Equal(t, `Usage of bundle count:`+common.AddrUsage, test.stderr.String()) + require.Equal(t, countUsage, test.stderr.String()) } func TestCountSynopsis(t *testing.T) { @@ -371,17 +433,19 @@ func TestCountSynopsis(t *testing.T) { func TestCount(t *testing.T) { for _, tt := range []struct { - name string - args []string - count int - expectedStdout string - expectedStderr string - serverErr error + name string + args []string + count int + expectedStdoutPretty string + expectedStdoutJSON string + expectedStderr string + serverErr error }{ { - name: "all bundles", - count: 2, - expectedStdout: "2 bundles\n", + name: "all bundles", + count: 2, + expectedStdoutPretty: "2 bundles\n", + expectedStdoutJSON: `{"count":2}`, }, { name: "all bundles server fails", @@ -390,9 +454,10 @@ func TestCount(t *testing.T) { serverErr: status.Error(codes.Internal, "some error"), }, { - name: "one bundle", - count: 1, - expectedStdout: "1 bundle\n", + name: "one bundle", + count: 1, + expectedStdoutPretty: "1 bundle\n", + expectedStdoutJSON: `{"count":1}`, }, { name: "one bundle server fails", @@ -401,45 +466,49 @@ func TestCount(t *testing.T) { serverErr: status.Error(codes.Internal, "some error"), }, { - name: "no bundles", - count: 0, - expectedStdout: "0 bundles\n", + name: "no bundles", + count: 0, + expectedStdoutPretty: "0 bundles\n", + expectedStdoutJSON: `{"count":0}`, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, NewCountCommandWithEnv) - test.server.err = tt.serverErr - bundles := []*types.Bundle{ - { - TrustDomain: "spiffe://domain1.test", - X509Authorities: []*types.X509Certificate{ - {Asn1: test.cert1.Raw}, - }, - JwtAuthorities: []*types.JWTKey{ - {KeyId: "KID", PublicKey: test.key1Pkix}, + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, NewCountCommandWithEnv) + test.server.err = tt.serverErr + bundles := []*types.Bundle{ + { + TrustDomain: "spiffe://domain1.test", + X509Authorities: []*types.X509Certificate{ + {Asn1: test.cert1.Raw}, + }, + JwtAuthorities: []*types.JWTKey{ + {KeyId: "KID", PublicKey: test.key1Pkix}, + }, }, - }, - { - TrustDomain: "spiffe://domain2.test", - X509Authorities: []*types.X509Certificate{ - {Asn1: test.cert2.Raw}, + { + TrustDomain: "spiffe://domain2.test", + X509Authorities: []*types.X509Certificate{ + {Asn1: test.cert2.Raw}, + }, }, - }, - } - - test.server.bundles = bundles[0:tt.count] - rc := test.client.Run(test.args(tt.args...)) - if tt.expectedStderr != "" { - require.Equal(t, tt.expectedStderr, test.stderr.String()) - require.Equal(t, 1, rc) - return - } - - require.Equal(t, 0, rc) - require.Empty(t, test.stderr.String()) - require.Equal(t, tt.expectedStdout, test.stdout.String()) - }) + } + test.server.bundles = bundles[0:tt.count] + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(args...)) + + if tt.expectedStderr != "" { + require.Equal(t, tt.expectedStderr, test.stderr.String()) + require.Equal(t, 1, rc) + return + } + assertOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Equal(t, 0, rc) + require.Empty(t, test.stderr.String()) + }) + } } } @@ -447,11 +516,7 @@ func TestListHelp(t *testing.T) { test := setupTest(t, newListCommand) test.client.Help() - require.Equal(t, `Usage of bundle list: - -format string - The format to list federated bundles. Either "pem" or "spiffe". (default "pem") - -id string - SPIFFE ID of the trust domain`+common.AddrUsage, test.stderr.String()) + require.Equal(t, listUsage, test.stderr.String()) } func TestListSynopsis(t *testing.T) { @@ -460,108 +525,168 @@ func TestListSynopsis(t *testing.T) { } func TestList(t *testing.T) { + allBundlesResultJSON := `{ + "bundles": [ + { + "trust_domain": "spiffe://domain1.test", + "x509_authorities": [ + { + "asn1": "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyvsCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09Xmakw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylAdZglS5kKnYigmwDh+/U=" + } + ], + "jwt_authorities": [ + { + "public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfK+wKTnKL7KFLM27lqq5DC+bxrVaH6rDV+IcCSEOeL7Cr6DdNBbFiVXnVMI8fTfTJexHG+6MPiFRRohCteTgog==", + "key_id": "KID", + "expires_at": "0" + } + ], + "refresh_hint": "0", + "sequence_number": "0" + }, + { + "trust_domain": "spiffe://domain2.test", + "x509_authorities": [ + { + "asn1": "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB8VbmlJ8YIuN9RuQ94PYanmkIRG7MkGV5mmrO6rFAv3SFd/uVlwYNkXrh0219eHUSD4o+4RGXoiMFJKysw5GK6jODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMi50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIQDMKwYtq+2ZoNyl4udPj7IMYIGX8yuCNRmh7m3d9tvoDgIgbS26wSwDjngGqdiHHL8fTcggdiIqWtxAqBLFrx8zNS4=" + } + ], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } + ], + "next_page_token": "" +}` + oneBundleResultJSON := `{ + "trust_domain": "spiffe://domain2.test", + "x509_authorities": [ + { + "asn1": "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB8VbmlJ8YIuN9RuQ94PYanmkIRG7MkGV5mmrO6rFAv3SFd/uVlwYNkXrh0219eHUSD4o+4RGXoiMFJKysw5GK6jODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMi50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIQDMKwYtq+2ZoNyl4udPj7IMYIGX8yuCNRmh7m3d9tvoDgIgbS26wSwDjngGqdiHHL8fTcggdiIqWtxAqBLFrx8zNS4=" + } + ], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" +}` for _, tt := range []struct { - name string - args []string - expectedStdout string - expectedStderr string - serverErr error + name string + args []string + expectedStdoutPretty string + expectedStdoutJSON string + expectedStderrPretty string + expectedStderrJSON string + serverErr error }{ { - name: "all bundles (default)", - expectedStdout: allBundlesPEM, + name: "all bundles (default)", + expectedStdoutPretty: allBundlesPEM, + expectedStdoutJSON: allBundlesResultJSON, }, { - name: "all bundles server fails", - expectedStderr: "Error: rpc error: code = Internal desc = some error\n", - serverErr: status.New(codes.Internal, "some error").Err(), + name: "all bundles server fails", + expectedStderrPretty: "Error: rpc error: code = Internal desc = some error\n", + expectedStderrJSON: "Error: rpc error: code = Internal desc = some error\n", + serverErr: status.New(codes.Internal, "some error").Err(), }, { - name: "all bundles invalid format", - args: []string{"-format", "invalid"}, - expectedStderr: "Error: invalid format: \"invalid\"\n", + name: "all bundles invalid bundle format", + args: []string{"-format", "invalid"}, + expectedStderrPretty: "Error: invalid format: \"invalid\"\n", + expectedStdoutJSON: allBundlesResultJSON, }, { - name: "all bundles (pem)", - args: []string{"-format", util.FormatPEM}, - expectedStdout: allBundlesPEM, + name: "all bundles (pem)", + args: []string{"-format", util.FormatPEM}, + expectedStdoutPretty: allBundlesPEM, + expectedStdoutJSON: allBundlesResultJSON, }, { - name: "all bundles (jwks)", - args: []string{"-format", util.FormatSPIFFE}, - expectedStdout: allBundlesJWKS, + name: "all bundles (jwks)", + args: []string{"-format", util.FormatSPIFFE}, + expectedStdoutPretty: allBundlesJWKS, + expectedStdoutJSON: allBundlesResultJSON, }, { - name: "one bundle (default)", - args: []string{"-id", "spiffe://domain2.test"}, - expectedStdout: cert2PEM, + name: "one bundle (default)", + args: []string{"-id", "spiffe://domain2.test"}, + expectedStdoutPretty: cert2PEM, + expectedStdoutJSON: oneBundleResultJSON, }, { - name: "one bundle server fails", - args: []string{"-id", "spiffe://domain2.test"}, - expectedStderr: "Error: rpc error: code = Internal desc = some error\n", - serverErr: status.New(codes.Internal, "some error").Err(), + name: "one bundle server fails", + args: []string{"-id", "spiffe://domain2.test"}, + expectedStderrPretty: "Error: rpc error: code = Internal desc = some error\n", + expectedStderrJSON: "Error: rpc error: code = Internal desc = some error\n", + serverErr: status.New(codes.Internal, "some error").Err(), }, { - name: "one bundle invalid format", - args: []string{"-id", "spiffe://domain2.test", "-format", "invalid"}, - expectedStderr: "Error: invalid format: \"invalid\"\n", + name: "one bundle invalid bundle format", + args: []string{"-id", "spiffe://domain2.test", "-format", "invalid"}, + expectedStderrPretty: "Error: invalid format: \"invalid\"\n", + expectedStdoutJSON: oneBundleResultJSON, }, { - name: "one bundle (pem)", - args: []string{"-id", "spiffe://domain2.test", "-format", util.FormatPEM}, - expectedStdout: cert2PEM, + name: "one bundle (pem)", + args: []string{"-id", "spiffe://domain2.test", "-format", util.FormatPEM}, + expectedStdoutPretty: cert2PEM, + expectedStdoutJSON: oneBundleResultJSON, }, { - name: "one bundle (jwks)", - args: []string{"-id", "spiffe://domain2.test", "-format", util.FormatSPIFFE}, - expectedStdout: cert2JWKS, + name: "one bundle (jwks)", + args: []string{"-id", "spiffe://domain2.test", "-format", util.FormatSPIFFE}, + expectedStdoutPretty: cert2JWKS, + expectedStdoutJSON: oneBundleResultJSON, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newListCommand) - test.server.err = tt.serverErr - test.server.bundles = []*types.Bundle{ - { - TrustDomain: "spiffe://domain1.test", - X509Authorities: []*types.X509Certificate{ - {Asn1: test.cert1.Raw}, - }, - JwtAuthorities: []*types.JWTKey{ - {KeyId: "KID", PublicKey: test.key1Pkix}, + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newListCommand) + test.server.err = tt.serverErr + test.server.bundles = []*types.Bundle{ + { + TrustDomain: "spiffe://domain1.test", + X509Authorities: []*types.X509Certificate{ + {Asn1: test.cert1.Raw}, + }, + JwtAuthorities: []*types.JWTKey{ + {KeyId: "KID", PublicKey: test.key1Pkix}, + }, }, - }, - { - TrustDomain: "spiffe://domain2.test", - X509Authorities: []*types.X509Certificate{ - {Asn1: test.cert2.Raw}, + { + TrustDomain: "spiffe://domain2.test", + X509Authorities: []*types.X509Certificate{ + {Asn1: test.cert2.Raw}, + }, }, - }, - } - - rc := test.client.Run(test.args(tt.args...)) - if tt.expectedStderr != "" { - require.Equal(t, tt.expectedStderr, test.stderr.String()) - require.Equal(t, 1, rc) - return - } - - require.Equal(t, 0, rc) - require.Empty(t, test.stderr.String()) - require.Equal(t, tt.expectedStdout, test.stdout.String()) - }) + } + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(args...)) + + if tt.expectedStderrPretty != "" && format == "pretty" { + require.Equal(t, tt.expectedStderrPretty, test.stderr.String()) + require.Equal(t, 1, rc) + return + } + if tt.expectedStderrJSON != "" && format == "json" { + require.Equal(t, tt.expectedStderrJSON, test.stderr.String()) + require.Equal(t, 1, rc) + return + } + assertOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Equal(t, 0, rc) + require.Empty(t, test.stderr.String()) + }) + } } } func TestDeleteHelp(t *testing.T) { test := setupTest(t, newDeleteCommand) test.client.Help() - require.Equal(t, `Usage of bundle delete: - -id string - SPIFFE ID of the trust domain - -mode string - Deletion mode: one of restrict, delete, or dissociate (default "restrict")`+common.AddrUsage, test.stderr.String()) + require.Equal(t, deleteUsage, test.stderr.String()) } func TestDeleteSynopsis(t *testing.T) { @@ -570,21 +695,35 @@ func TestDeleteSynopsis(t *testing.T) { } func TestDelete(t *testing.T) { + deleteResultJSON := `{ + "results": [ + { + "status": { + "code": 0, + "message": "ok" + }, + "trust_domain": "domain1.test" + } + ] +}` for _, tt := range []struct { - name string - args []string - expectedStderr string - expectedStdout string - deleteResults []*bundlev1.BatchDeleteFederatedBundleResponse_Result - mode bundlev1.BatchDeleteFederatedBundleRequest_Mode - toDelete []string - serverErr error + name string + args []string + expectedStderrPretty string + expectedStderrJSON string + expectedStdoutPretty string + expectedStdoutJSON string + deleteResults []*bundlev1.BatchDeleteFederatedBundleResponse_Result + mode bundlev1.BatchDeleteFederatedBundleRequest_Mode + toDelete []string + serverErr error }{ { - name: "success default mode", - args: []string{"-id", "spiffe://domain1.test"}, - expectedStdout: "bundle deleted.\n", - toDelete: []string{"spiffe://domain1.test"}, + name: "success default mode", + args: []string{"-id", "spiffe://domain1.test"}, + expectedStdoutPretty: "bundle deleted.\n", + expectedStdoutJSON: deleteResultJSON, + toDelete: []string{"spiffe://domain1.test"}, deleteResults: []*bundlev1.BatchDeleteFederatedBundleResponse_Result{ { Status: &types.Status{ @@ -597,15 +736,17 @@ func TestDelete(t *testing.T) { }, }, { - name: "no id", - expectedStderr: "Error: id is required\n", + name: "no id", + expectedStderrPretty: "Error: id is required\n", + expectedStderrJSON: "Error: id is required\n", }, { - name: "success RESTRICT mode", - args: []string{"-id", "spiffe://domain1.test", "-mode", "restrict"}, - expectedStdout: "bundle deleted.\n", - mode: bundlev1.BatchDeleteFederatedBundleRequest_RESTRICT, - toDelete: []string{"spiffe://domain1.test"}, + name: "success RESTRICT mode", + args: []string{"-id", "spiffe://domain1.test", "-mode", "restrict"}, + expectedStdoutPretty: "bundle deleted.\n", + expectedStdoutJSON: deleteResultJSON, + mode: bundlev1.BatchDeleteFederatedBundleRequest_RESTRICT, + toDelete: []string{"spiffe://domain1.test"}, deleteResults: []*bundlev1.BatchDeleteFederatedBundleResponse_Result{ { Status: &types.Status{ @@ -618,11 +759,12 @@ func TestDelete(t *testing.T) { }, }, { - name: "success DISSOCIATE mode", - args: []string{"-id", "spiffe://domain1.test", "-mode", "dissociate"}, - expectedStdout: "bundle deleted.\n", - mode: bundlev1.BatchDeleteFederatedBundleRequest_DISSOCIATE, - toDelete: []string{"spiffe://domain1.test"}, + name: "success DISSOCIATE mode", + args: []string{"-id", "spiffe://domain1.test", "-mode", "dissociate"}, + expectedStdoutPretty: "bundle deleted.\n", + expectedStdoutJSON: deleteResultJSON, + mode: bundlev1.BatchDeleteFederatedBundleRequest_DISSOCIATE, + toDelete: []string{"spiffe://domain1.test"}, deleteResults: []*bundlev1.BatchDeleteFederatedBundleResponse_Result{ { Status: &types.Status{ @@ -635,11 +777,12 @@ func TestDelete(t *testing.T) { }, }, { - name: "success DELETE mode", - args: []string{"-id", "spiffe://domain1.test", "-mode", "delete"}, - expectedStdout: "bundle deleted.\n", - mode: bundlev1.BatchDeleteFederatedBundleRequest_DELETE, - toDelete: []string{"spiffe://domain1.test"}, + name: "success DELETE mode", + args: []string{"-id", "spiffe://domain1.test", "-mode", "delete"}, + expectedStdoutPretty: "bundle deleted.\n", + expectedStdoutJSON: deleteResultJSON, + mode: bundlev1.BatchDeleteFederatedBundleRequest_DELETE, + toDelete: []string{"spiffe://domain1.test"}, deleteResults: []*bundlev1.BatchDeleteFederatedBundleResponse_Result{ { Status: &types.Status{ @@ -652,15 +795,17 @@ func TestDelete(t *testing.T) { }, }, { - name: "invalid mode", - args: []string{"-id", "spiffe://domain1.test", "-mode", "invalid"}, - expectedStderr: "Error: unsupported mode \"invalid\"\n", + name: "invalid mode", + args: []string{"-id", "spiffe://domain1.test", "-mode", "invalid"}, + expectedStderrPretty: "Error: unsupported mode \"invalid\"\n", + expectedStderrJSON: "Error: unsupported mode \"invalid\"\n", }, { - name: "server fails", - args: []string{"-id", "spiffe://domain1.test"}, - expectedStderr: "Error: failed to delete federated bundle: rpc error: code = Internal desc = some error\n", - serverErr: status.New(codes.Internal, "some error").Err(), + name: "server fails", + args: []string{"-id", "spiffe://domain1.test"}, + expectedStderrPretty: "Error: failed to delete federated bundle: rpc error: code = Internal desc = some error\n", + expectedStderrJSON: "Error: failed to delete federated bundle: rpc error: code = Internal desc = some error\n", + serverErr: status.New(codes.Internal, "some error").Err(), }, { name: "fails to delete", @@ -676,28 +821,51 @@ func TestDelete(t *testing.T) { TrustDomain: "domain1.test", }, }, - expectedStderr: "Error: failed to delete federated bundle \"domain1.test\": some error\n", + expectedStderrPretty: "Error: failed to delete federated bundle \"domain1.test\": some error\n", + expectedStdoutJSON: `{"results":[{"status":{"code":13,"message":"some error"},"trust_domain":"domain1.test"}]}`, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newDeleteCommand) - test.server.deleteResults = tt.deleteResults - test.server.err = tt.serverErr - test.server.mode = tt.mode - test.server.toDelete = tt.toDelete - - rc := test.client.Run(test.args(tt.args...)) - if tt.expectedStderr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expectedStderr, test.stderr.String()) - - return - } - - require.Empty(t, test.stderr.String()) - require.Equal(t, 0, rc) - require.Equal(t, tt.expectedStdout, test.stdout.String()) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newDeleteCommand) + test.server.deleteResults = tt.deleteResults + test.server.err = tt.serverErr + test.server.mode = tt.mode + test.server.toDelete = tt.toDelete + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(args...)) + + if tt.expectedStderrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectedStderrPretty, test.stderr.String()) + + return + } + if tt.expectedStderrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectedStderrJSON, test.stderr.String()) + + return + } + assertOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + require.Empty(t, test.stderr.String()) + require.Equal(t, 0, rc) + }) + } + } +} + +func assertOutputBasedOnFormat(t *testing.T, format, stdoutString string, expectedStdoutPretty, expectedStdoutJSON string) { + switch format { + case "pretty": + require.Contains(t, stdoutString, expectedStdoutPretty) + case "json": + if expectedStdoutJSON != "" { + require.JSONEq(t, expectedStdoutJSON, stdoutString) + } else { + require.Empty(t, stdoutString) + } } } diff --git a/cmd/spire-server/cli/bundle/bundle_windows_test.go b/cmd/spire-server/cli/bundle/bundle_windows_test.go index a10a5a8501..c1e002f24d 100644 --- a/cmd/spire-server/cli/bundle/bundle_windows_test.go +++ b/cmd/spire-server/cli/bundle/bundle_windows_test.go @@ -11,7 +11,43 @@ var ( SPIFFE ID of the trust domain -namedPipeName string Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. -path string Path to the bundle data +` + showUsage = `Usage of bundle show: + -format string + The format to show the bundle (only pretty output format supports this flag). Either "pem" or "spiffe". (default "pem") + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + countUsage = `Usage of bundle count: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + listUsage = `Usage of bundle list: + -format string + The format to list federated bundles (only pretty output format supports this flag). Either "pem" or "spiffe". (default "pem") + -id string + SPIFFE ID of the trust domain + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + deleteUsage = `Usage of bundle delete: + -id string + SPIFFE ID of the trust domain + -mode string + Deletion mode: one of restrict, delete, or dissociate (default "restrict") + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. ` ) diff --git a/cmd/spire-server/cli/bundle/count.go b/cmd/spire-server/cli/bundle/count.go index fb6933358f..82fba385bf 100644 --- a/cmd/spire-server/cli/bundle/count.go +++ b/cmd/spire-server/cli/bundle/count.go @@ -5,50 +5,61 @@ import ( "fmt" "github.com/mitchellh/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" "golang.org/x/net/context" ) -type countCommand struct{} +type countCommand struct { + env *commoncli.Env + printer cliprinter.Printer +} // NewCountCommand creates a new "count" subcommand for "bundle" command. func NewCountCommand() cli.Command { - return NewCountCommandWithEnv(common_cli.DefaultEnv) + return NewCountCommandWithEnv(commoncli.DefaultEnv) } // NewCountCommandWithEnv creates a new "count" subcommand for "bundle" command // using the environment specified. -func NewCountCommandWithEnv(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(countCommand)) +func NewCountCommandWithEnv(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &countCommand{env: env}) } func (*countCommand) Name() string { return "bundle count" } -func (countCommand) Synopsis() string { +func (*countCommand) Synopsis() string { return "Count bundles" } // Run counts attested bundles -func (c *countCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *countCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { bundleClient := serverClient.NewBundleClient() - countResponse, err := bundleClient.CountBundles(ctx, &bundlev1.CountBundlesRequest{}) + countResp, err := bundleClient.CountBundles(ctx, &bundlev1.CountBundlesRequest{}) if err != nil { return err } - count := int(countResponse.Count) - msg := fmt.Sprintf("%d ", count) - msg = util.Pluralizer(msg, "bundle", "bundles", count) - env.Println(msg) - - return nil + return c.printer.PrintProto(countResp) } func (c *countCommand) AppendFlags(fs *flag.FlagSet) { + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintCount) +} + +func prettyPrintCount(env *commoncli.Env, results ...interface{}) error { + countResp, ok := results[0].(*bundlev1.CountBundlesResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + count := int(countResp.Count) + msg := fmt.Sprintf("%d ", count) + msg = util.Pluralizer(msg, "bundle", "bundles", count) + return env.Println(msg) } diff --git a/cmd/spire-server/cli/bundle/delete.go b/cmd/spire-server/cli/bundle/delete.go index 4ebc0c2646..7f24e8fc99 100644 --- a/cmd/spire-server/cli/bundle/delete.go +++ b/cmd/spire-server/cli/bundle/delete.go @@ -9,7 +9,8 @@ import ( "github.com/mitchellh/cli" bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "google.golang.org/grpc/codes" ) @@ -21,19 +22,21 @@ const ( // NewDeleteCommand creates a new "delete" subcommand for "bundle" command. func NewDeleteCommand() cli.Command { - return newDeleteCommand(common_cli.DefaultEnv) + return newDeleteCommand(commoncli.DefaultEnv) } -func newDeleteCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(deleteCommand)) +func newDeleteCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &deleteCommand{env: env}) } type deleteCommand struct { + env *commoncli.Env // SPIFFE ID of the trust domain bundle id string - // Deletion mode mode string + // Command printer + printer cliprinter.Printer } func (c *deleteCommand) Name() string { @@ -47,9 +50,10 @@ func (c *deleteCommand) Synopsis() string { func (c *deleteCommand) AppendFlags(fs *flag.FlagSet) { fs.StringVar(&c.id, "id", "", "SPIFFE ID of the trust domain") fs.StringVar(&c.mode, "mode", deleteBundleRestrict, fmt.Sprintf("Deletion mode: one of %s, %s, or %s", deleteBundleRestrict, deleteBundleDelete, deleteBundleDissociate)) + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintDelete) } -func (c *deleteCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *deleteCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if c.id == "" { return errors.New("id is required") } @@ -69,7 +73,16 @@ func (c *deleteCommand) Run(ctx context.Context, env *common_cli.Env, serverClie if err != nil { return fmt.Errorf("failed to delete federated bundle: %w", err) } - result := resp.Results[0] + + return c.printer.PrintProto(resp) +} + +func prettyPrintDelete(env *commoncli.Env, results ...interface{}) error { + deleteResp, ok := results[0].(*bundlev1.BatchDeleteFederatedBundleResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + result := deleteResp.Results[0] switch result.Status.Code { case int32(codes.OK): env.Println("bundle deleted.") diff --git a/cmd/spire-server/cli/bundle/list.go b/cmd/spire-server/cli/bundle/list.go index b6c1565671..a3c671b81b 100644 --- a/cmd/spire-server/cli/bundle/list.go +++ b/cmd/spire-server/cli/bundle/list.go @@ -7,22 +7,26 @@ import ( "github.com/mitchellh/cli" bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1" + "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" ) // NewListCommand creates a new "list" subcommand for "bundle" command. func NewListCommand() cli.Command { - return newListCommand(common_cli.DefaultEnv) + return newListCommand(commoncli.DefaultEnv) } -func newListCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(listCommand)) +func newListCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &listCommand{env: env}) } type listCommand struct { - id string // SPIFFE ID of the trust bundle - format string + env *commoncli.Env + id string // SPIFFE ID of the trust bundle + bundleFormat string + printer cliprinter.Printer } func (c *listCommand) Name() string { @@ -35,10 +39,11 @@ func (c *listCommand) Synopsis() string { func (c *listCommand) AppendFlags(fs *flag.FlagSet) { fs.StringVar(&c.id, "id", "", "SPIFFE ID of the trust domain") - fs.StringVar(&c.format, "format", util.FormatPEM, fmt.Sprintf("The format to list federated bundles. Either %q or %q.", util.FormatPEM, util.FormatSPIFFE)) + fs.StringVar(&c.bundleFormat, "format", util.FormatPEM, fmt.Sprintf("The format to list federated bundles (only pretty output format supports this flag). Either %q or %q.", util.FormatPEM, util.FormatSPIFFE)) + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, c.prettyPrintList) } -func (c *listCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *listCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { bundleClient := serverClient.NewBundleClient() if c.id != "" { resp, err := bundleClient.GetFederatedBundle(ctx, &bundlev1.GetFederatedBundleRequest{ @@ -47,7 +52,7 @@ func (c *listCommand) Run(ctx context.Context, env *common_cli.Env, serverClient if err != nil { return err } - return printBundleWithFormat(env.Stdout, resp, c.format, false) + return c.printer.PrintProto(resp) } resp, err := bundleClient.ListFederatedBundles(ctx, &bundlev1.ListFederatedBundlesRequest{}) @@ -55,16 +60,27 @@ func (c *listCommand) Run(ctx context.Context, env *common_cli.Env, serverClient return err } - for i, b := range resp.Bundles { - if i != 0 { - if err := env.Println(); err != nil { - return err + return c.printer.PrintProto(resp) +} + +func (c *listCommand) prettyPrintList(env *commoncli.Env, results ...interface{}) error { + if listResp, ok := results[0].(*bundlev1.ListFederatedBundlesResponse); ok { + for i, bundle := range listResp.Bundles { + if i != 0 { + if err := env.Println(); err != nil { + return err + } } - } - if err := printBundleWithFormat(env.Stdout, b, c.format, true); err != nil { - return err + if err := printBundleWithFormat(env.Stdout, bundle, c.bundleFormat, true); err != nil { + return err + } } + return nil } - return nil + if resp, ok := results[0].(*types.Bundle); ok { + return printBundleWithFormat(env.Stdout, resp, c.bundleFormat, false) + } + + return cliprinter.ErrInternalCustomPrettyFunc } diff --git a/cmd/spire-server/cli/bundle/set.go b/cmd/spire-server/cli/bundle/set.go index ad80c210b7..3ae9c010f9 100644 --- a/cmd/spire-server/cli/bundle/set.go +++ b/cmd/spire-server/cli/bundle/set.go @@ -11,6 +11,7 @@ import ( "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" common_cli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "google.golang.org/grpc/codes" ) @@ -20,17 +21,17 @@ func NewSetCommand() cli.Command { } func newSetCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(setCommand)) + return util.AdaptCommand(env, &setCommand{env: env}) } type setCommand struct { + env *common_cli.Env // SPIFFE ID of the trust bundle id string - // Path to the bundle on disk (optional). If empty, reads from stdin. - path string - - format string + path string + bundleFormat string + printer cliprinter.Printer } func (c *setCommand) Name() string { @@ -44,7 +45,8 @@ func (c *setCommand) Synopsis() string { func (c *setCommand) AppendFlags(fs *flag.FlagSet) { fs.StringVar(&c.id, "id", "", "SPIFFE ID of the trust domain") fs.StringVar(&c.path, "path", "", "Path to the bundle data") - fs.StringVar(&c.format, "format", util.FormatPEM, fmt.Sprintf("The format of the bundle data. Either %q or %q.", util.FormatPEM, util.FormatSPIFFE)) + fs.StringVar(&c.bundleFormat, "format", util.FormatPEM, fmt.Sprintf("The format of the bundle data. Either %q or %q.", util.FormatPEM, util.FormatSPIFFE)) + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintSet) } func (c *setCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { @@ -52,7 +54,7 @@ func (c *setCommand) Run(ctx context.Context, env *common_cli.Env, serverClient return errors.New("id flag is required") } - format, err := validateFormat(c.format) + bundleFormat, err := validateFormat(c.bundleFormat) if err != nil { return err } @@ -62,7 +64,7 @@ func (c *setCommand) Run(ctx context.Context, env *common_cli.Env, serverClient return fmt.Errorf("unable to load bundle data: %w", err) } - bundle, err := util.ParseBundle(bundleBytes, format, c.id) + bundle, err := util.ParseBundle(bundleBytes, bundleFormat, c.id) if err != nil { return err } @@ -75,7 +77,15 @@ func (c *setCommand) Run(ctx context.Context, env *common_cli.Env, serverClient return fmt.Errorf("failed to set federated bundle: %w", err) } - result := resp.Results[0] + return c.printer.PrintProto(resp) +} + +func prettyPrintSet(env *common_cli.Env, results ...interface{}) error { + setResp, ok := results[0].(*bundlev1.BatchSetFederatedBundleResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + result := setResp.Results[0] switch result.Status.Code { case int32(codes.OK): env.Println("bundle set.") diff --git a/cmd/spire-server/cli/bundle/show.go b/cmd/spire-server/cli/bundle/show.go index ab0a337fc5..b36f3777a6 100644 --- a/cmd/spire-server/cli/bundle/show.go +++ b/cmd/spire-server/cli/bundle/show.go @@ -7,8 +7,10 @@ import ( "github.com/mitchellh/cli" bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1" + "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" common_cli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" ) // NewShowCommand creates a new "show" subcommand for "bundle" command. @@ -17,11 +19,13 @@ func NewShowCommand() cli.Command { } func newShowCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(showCommand)) + return util.AdaptCommand(env, &showCommand{env: env}) } type showCommand struct { - format string + env *common_cli.Env + bundleFormat string + printer cliprinter.Printer } func (c *showCommand) Name() string { @@ -33,7 +37,8 @@ func (c *showCommand) Synopsis() string { } func (c *showCommand) AppendFlags(fs *flag.FlagSet) { - fs.StringVar(&c.format, "format", util.FormatPEM, fmt.Sprintf("The format to show the bundle. Either %q or %q.", util.FormatPEM, util.FormatSPIFFE)) + fs.StringVar(&c.bundleFormat, "format", util.FormatPEM, fmt.Sprintf("The format to show the bundle (only pretty output format supports this flag). Either %q or %q.", util.FormatPEM, util.FormatSPIFFE)) + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, c.prettyPrintBundle) } func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { @@ -43,5 +48,13 @@ func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient return err } - return printBundleWithFormat(env.Stdout, resp, c.format, false) + return c.printer.PrintProto(resp) +} + +func (c *showCommand) prettyPrintBundle(env *common_cli.Env, results ...interface{}) error { + showResp, ok := results[0].(*types.Bundle) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + return printBundleWithFormat(env.Stdout, showResp, c.bundleFormat, false) } diff --git a/cmd/spire-server/cli/cli.go b/cmd/spire-server/cli/cli.go index a1e6c4352c..025b17bd2c 100644 --- a/cmd/spire-server/cli/cli.go +++ b/cmd/spire-server/cli/cli.go @@ -1,6 +1,7 @@ package cli import ( + "context" stdlog "log" "github.com/mitchellh/cli" @@ -25,7 +26,7 @@ type CLI struct { } // Run configures the server CLI commands and subcommands. -func (cc *CLI) Run(args []string) int { +func (cc *CLI) Run(ctx context.Context, args []string) int { c := cli.NewCLI("spire-server", version.Version()) c.Args = args c.Commands = map[string]cli.CommandFactory{ @@ -93,7 +94,7 @@ func (cc *CLI) Run(args []string) int { return federation.NewUpdateCommand(), nil }, "run": func() (cli.Command, error) { - return run.NewRunCommand(cc.LogOptions, cc.AllowUnknownConfig), nil + return run.NewRunCommand(ctx, cc.LogOptions, cc.AllowUnknownConfig), nil }, "token generate": func() (cli.Command, error) { return token.NewGenerateCommand(), nil diff --git a/cmd/spire-server/cli/entry/count.go b/cmd/spire-server/cli/entry/count.go index 102b67aeaf..646cbe80a2 100644 --- a/cmd/spire-server/cli/entry/count.go +++ b/cmd/spire-server/cli/entry/count.go @@ -5,50 +5,63 @@ import ( "fmt" "github.com/mitchellh/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" "golang.org/x/net/context" ) -type countCommand struct{} +type countCommand struct { + printer cliprinter.Printer + env *commoncli.Env +} // NewCountCommand creates a new "count" subcommand for "entry" command. func NewCountCommand() cli.Command { - return NewCountCommandWithEnv(common_cli.DefaultEnv) + return NewCountCommandWithEnv(commoncli.DefaultEnv) } // NewCountCommandWithEnv creates a new "count" subcommand for "entry" command // using the environment specified. -func NewCountCommandWithEnv(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(countCommand)) +func NewCountCommandWithEnv(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &countCommand{env: env}) } func (*countCommand) Name() string { return "entry count" } -func (countCommand) Synopsis() string { +func (*countCommand) Synopsis() string { return "Count registration entries" } // Run counts attested entries -func (c *countCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *countCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { entryClient := serverClient.NewEntryClient() countResponse, err := entryClient.CountEntries(ctx, &entryv1.CountEntriesRequest{}) if err != nil { return err } - count := int(countResponse.Count) + return c.printer.PrintProto(countResponse) +} + +func (c *countCommand) AppendFlags(fs *flag.FlagSet) { + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, c.prettyPrintCount) +} + +func (c *countCommand) prettyPrintCount(env *commoncli.Env, results ...interface{}) error { + countResp, ok := results[0].(*entryv1.CountEntriesResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + count := int(countResp.Count) msg := fmt.Sprintf("%d registration ", count) msg = util.Pluralizer(msg, "entry", "entries", count) env.Println(msg) return nil } - -func (c *countCommand) AppendFlags(fs *flag.FlagSet) { -} diff --git a/cmd/spire-server/cli/entry/count_test.go b/cmd/spire-server/cli/entry/count_test.go index 0e2036071b..cfff9ca6f7 100644 --- a/cmd/spire-server/cli/entry/count_test.go +++ b/cmd/spire-server/cli/entry/count_test.go @@ -1,10 +1,10 @@ package entry import ( + "fmt" "testing" entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -14,7 +14,7 @@ func TestCountHelp(t *testing.T) { test := setupTest(t, NewCountCommandWithEnv) test.client.Help() - require.Equal(t, `Usage of entry count:`+common.AddrUsage, test.stderr.String()) + require.Equal(t, countUsage, test.stderr.String()) } func TestCountSynopsis(t *testing.T) { @@ -33,28 +33,33 @@ func TestCount(t *testing.T) { args []string fakeCountResp *entryv1.CountEntriesResponse serverErr error - expOut string + expOutPretty string + expOutJSON string expErr string }{ { name: "4 entries", fakeCountResp: fakeResp4, - expOut: "4 registration entries\n", + expOutPretty: "4 registration entries\n", + expOutJSON: `{"count":4}`, }, { name: "2 entries", fakeCountResp: fakeResp2, - expOut: "2 registration entries\n", + expOutPretty: "2 registration entries\n", + expOutJSON: `{"count":2}`, }, { name: "1 entry", fakeCountResp: fakeResp1, - expOut: "1 registration entry\n", + expOutPretty: "1 registration entry\n", + expOutJSON: `{"count":1}`, }, { name: "0 entries", fakeCountResp: fakeResp0, - expOut: "0 registration entries\n", + expOutPretty: "0 registration entries\n", + expOutJSON: `{"count":0}`, }, { name: "Server error", @@ -62,21 +67,21 @@ func TestCount(t *testing.T) { expErr: "Error: rpc error: code = Internal desc = internal server error\n", }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, NewCountCommandWithEnv) - test.server.err = tt.serverErr - test.server.countEntriesResp = tt.fakeCountResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, NewCountCommandWithEnv) + test.server.err = tt.serverErr + test.server.countEntriesResp = tt.fakeCountResp - rc := test.client.Run(test.args(tt.args...)) - if tt.expErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expErr, test.stderr.String()) - return - } - - require.Equal(t, 0, rc) - require.Equal(t, tt.expOut, test.stdout.String()) - }) + rc := test.client.Run(test.args(tt.args...)) + if tt.expErr != "" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErr, test.stderr.String()) + return + } + requireOutputBasedOnFormat(t, test.stdout.String(), format, tt.expOutPretty, tt.expOutJSON) + require.Equal(t, 0, rc) + }) + } } } diff --git a/cmd/spire-server/cli/entry/create.go b/cmd/spire-server/cli/entry/create.go index 18b02b2c2f..483edc690b 100644 --- a/cmd/spire-server/cli/entry/create.go +++ b/cmd/spire-server/cli/entry/create.go @@ -8,7 +8,8 @@ import ( entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "github.com/spiffe/spire/pkg/common/idutil" "google.golang.org/grpc/codes" @@ -17,11 +18,11 @@ import ( // NewCreateCommand creates a new "create" subcommand for "entry" command. func NewCreateCommand() cli.Command { - return newCreateCommand(common_cli.DefaultEnv) + return newCreateCommand(commoncli.DefaultEnv) } -func newCreateCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(createCommand)) +func newCreateCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &createCommand{env: env}) } type createCommand struct { @@ -39,9 +40,17 @@ type createCommand struct { // Workload spiffeID spiffeID string - // TTL for certificates issued to this workload + // TTL for x509 and JWT SVIDs issued to this workload, unless type specific TTLs are set. + // This field is deprecated in favor of the x509SVIDTTL and jwtSVIDTTL fields and will be + // removed in a future release. ttl int + // TTL for x509 SVIDs issued to this workload + x509SVIDTTL int + + // TTL for JWT SVIDs issued to this workload + jwtSVIDTTL int + // List of SPIFFE IDs of trust domains the registration entry is federated with federatesWith StringsFlag @@ -62,6 +71,10 @@ type createCommand struct { // storeSVID determines if the issued SVID must be stored through an SVIDStore plugin storeSVID bool + + printer cliprinter.Printer + + env *commoncli.Env } func (*createCommand) Name() string { @@ -75,7 +88,9 @@ func (*createCommand) Synopsis() string { func (c *createCommand) AppendFlags(f *flag.FlagSet) { f.StringVar(&c.parentID, "parentID", "", "The SPIFFE ID of this record's parent") f.StringVar(&c.spiffeID, "spiffeID", "", "The SPIFFE ID that this record represents") - f.IntVar(&c.ttl, "ttl", 0, "The lifetime, in seconds, for SVIDs issued based on this registration entry") + f.IntVar(&c.ttl, "ttl", 0, "The lifetime, in seconds, for SVIDs issued based on this registration entry. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version") + f.IntVar(&c.x509SVIDTTL, "x509SVIDTTL", 0, "The lifetime, in seconds, for x509-SVIDs issued based on this registration entry. Overrides ttl flag") + f.IntVar(&c.jwtSVIDTTL, "jwtSVIDTTL", 0, "The lifetime, in seconds, for JWT-SVIDs issued based on this registration entry. Overrides ttl flag") f.StringVar(&c.path, "data", "", "Path to a file containing registration JSON (optional). If set to '-', read the JSON from stdin.") f.Var(&c.selectors, "selector", "A colon-delimited type:value selector. Can be used more than once") f.Var(&c.federatesWith, "federatesWith", "SPIFFE ID of a trust domain to federate with. Can be used more than once") @@ -85,9 +100,10 @@ func (c *createCommand) AppendFlags(f *flag.FlagSet) { f.BoolVar(&c.downstream, "downstream", false, "A boolean value that, when set, indicates that the entry describes a downstream SPIRE server") f.Int64Var(&c.entryExpiry, "entryExpiry", 0, "An expiry, from epoch in seconds, for the resulting registration entry to be pruned") f.Var(&c.dnsNames, "dns", "A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once") + cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, prettyPrintCreate) } -func (c *createCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *createCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if err := c.validate(); err != nil { return err } @@ -103,29 +119,12 @@ func (c *createCommand) Run(ctx context.Context, env *common_cli.Env, serverClie return err } - succeeded, failed, err := createEntries(ctx, serverClient.NewEntryClient(), entries) + resp, err := createEntries(ctx, serverClient.NewEntryClient(), entries) if err != nil { return err } - // Print entries that succeeded to be created - for _, r := range succeeded { - printEntry(r.Entry, env.Printf) - } - - // Print entries that failed to be created - for _, r := range failed { - env.ErrPrintf("Failed to create the following entry (code: %s, msg: %q):\n", - codes.Code(r.Status.Code), - r.Status.Message) - printEntry(r.Entry, env.ErrPrintf) - } - - if len(failed) > 0 { - return errors.New("failed to create one or more entries") - } - - return nil + return c.printer.PrintProto(resp) } // validate performs basic validation, even on fields that we @@ -156,6 +155,18 @@ func (c *createCommand) validate() (err error) { return errors.New("a positive TTL is required") } + if c.x509SVIDTTL < 0 { + return errors.New("a positive x509-SVID TTL is required") + } + + if c.jwtSVIDTTL < 0 { + return errors.New("a positive JWT-SVID TTL is required") + } + + if c.ttl > 0 && (c.x509SVIDTTL > 0 || c.jwtSVIDTTL > 0) { + return errors.New("use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag") + } + return nil } @@ -172,13 +183,26 @@ func (c *createCommand) parseConfig() ([]*types.Entry, error) { } e := &types.Entry{ - ParentId: parentID, - SpiffeId: spiffeID, - Ttl: int32(c.ttl), - Downstream: c.downstream, - ExpiresAt: c.entryExpiry, - DnsNames: c.dnsNames, - StoreSvid: c.storeSVID, + ParentId: parentID, + SpiffeId: spiffeID, + Downstream: c.downstream, + ExpiresAt: c.entryExpiry, + DnsNames: c.dnsNames, + StoreSvid: c.storeSVID, + X509SvidTtl: int32(c.x509SVIDTTL), + JwtSvidTtl: int32(c.jwtSVIDTTL), + } + + // c.ttl is deprecated but usable if the new c.x509Svid field is not used. + // c.ttl should not be used to set the jwtSVIDTTL value because the previous + // behavior was to have a hard-coded 5 minute JWT TTL no matter what the value + // of ttl was set to. + // validate(...) ensures that either the new fields or the deprecated field is + // used, but never a mixture. + // + // https://github.com/spiffe/spire/issues/2700 + if e.X509SvidTtl == 0 { + e.X509SvidTtl = int32(c.ttl) } selectors := []*types.Selector{} @@ -197,25 +221,21 @@ func (c *createCommand) parseConfig() ([]*types.Entry, error) { return []*types.Entry{e}, nil } -func createEntries(ctx context.Context, c entryv1.EntryClient, entries []*types.Entry) (succeeded, failed []*entryv1.BatchCreateEntryResponse_Result, err error) { - resp, err := c.BatchCreateEntry(ctx, &entryv1.BatchCreateEntryRequest{Entries: entries}) +func createEntries(ctx context.Context, c entryv1.EntryClient, entries []*types.Entry) (resp *entryv1.BatchCreateEntryResponse, err error) { + resp, err = c.BatchCreateEntry(ctx, &entryv1.BatchCreateEntryRequest{Entries: entries}) if err != nil { - return nil, nil, err + return } for i, r := range resp.Results { - switch r.Status.Code { - case int32(codes.OK): - succeeded = append(succeeded, r) - default: + if r.Status.Code != int32(codes.OK) { // The Entry API does not include in the results the entries that // failed to be created, so we populate them from the request data. r.Entry = entries[i] - failed = append(failed, r) } } - return succeeded, failed, nil + return } func getParentID(config *createCommand, td string) (*types.SPIFFEID, error) { @@ -228,3 +248,37 @@ func getParentID(config *createCommand, td string) (*types.SPIFFEID, error) { } return idStringToProto(config.parentID) } + +func prettyPrintCreate(env *commoncli.Env, results ...interface{}) error { + var succeeded, failed []*entryv1.BatchCreateEntryResponse_Result + createResp, ok := results[0].(*entryv1.BatchCreateEntryResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + + for _, r := range createResp.Results { + switch r.Status.Code { + case int32(codes.OK): + succeeded = append(succeeded, r) + default: + failed = append(failed, r) + } + } + + for _, r := range succeeded { + printEntry(r.Entry, env.Printf) + } + + for _, r := range failed { + env.ErrPrintf("Failed to create the following entry (code: %s, msg: %q):\n", + codes.Code(r.Status.Code), + r.Status.Message) + printEntry(r.Entry, env.ErrPrintf) + } + + if len(failed) > 0 { + return errors.New("failed to create one or more entries") + } + + return nil +} diff --git a/cmd/spire-server/cli/entry/create_test.go b/cmd/spire-server/cli/entry/create_test.go index dad5bbaada..2769ea39c1 100644 --- a/cmd/spire-server/cli/entry/create_test.go +++ b/cmd/spire-server/cli/entry/create_test.go @@ -36,7 +36,35 @@ func TestCreate(t *testing.T) { {Type: "zebra", Value: "zebra:2000"}, {Type: "alpha", Value: "alpha:2000"}, }, - Ttl: 60, + X509SvidTtl: 60, + JwtSvidTtl: 30, + FederatesWith: []string{"spiffe://domaina.test", "spiffe://domainb.test"}, + Admin: true, + ExpiresAt: 1552410266, + DnsNames: []string{"unu1000", "ung1000"}, + Downstream: true, + StoreSvid: true, + }, + Status: &types.Status{ + Code: int32(codes.OK), + Message: "OK", + }, + }, + }, + } + + fakeRespOKFromCmd2 := &entryv1.BatchCreateEntryResponse{ + Results: []*entryv1.BatchCreateEntryResponse_Result{ + { + Entry: &types.Entry{ + Id: "entry-id", + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/parent"}, + Selectors: []*types.Selector{ + {Type: "zebra", Value: "zebra:2000"}, + {Type: "alpha", Value: "alpha:2000"}, + }, + X509SvidTtl: 60, FederatesWith: []string{"spiffe://domaina.test", "spiffe://domainb.test"}, Admin: true, ExpiresAt: 1552410266, @@ -56,12 +84,13 @@ func TestCreate(t *testing.T) { Results: []*entryv1.BatchCreateEntryResponse_Result{ { Entry: &types.Entry{ - Id: "entry-id-1", - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, - Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, - Ttl: 200, - Admin: true, + Id: "entry-id-1", + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, + Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, + X509SvidTtl: 200, + JwtSvidTtl: 30, + Admin: true, }, Status: &types.Status{ Code: int32(codes.OK), @@ -70,11 +99,12 @@ func TestCreate(t *testing.T) { }, { Entry: &types.Entry{ - Id: "entry-id-2", - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, - Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, - Ttl: 200, + Id: "entry-id-2", + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, + Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, + X509SvidTtl: 200, + JwtSvidTtl: 30, }, Status: &types.Status{ Code: int32(codes.OK), @@ -90,8 +120,9 @@ func TestCreate(t *testing.T) { {Type: "type", Value: "key1:value"}, {Type: "type", Value: "key2:value"}, }, - StoreSvid: true, - Ttl: 200, + StoreSvid: true, + X509SvidTtl: 200, + JwtSvidTtl: 30, }, Status: &types.Status{ Code: int32(codes.OK), @@ -120,37 +151,63 @@ func TestCreate(t *testing.T) { fakeResp *entryv1.BatchCreateEntryResponse serverErr error - expOut string - expErr string + expOutPretty string + expOutJSON string + expErrJSON string + expErrPretty string }{ { - name: "Missing selectors", - expErr: "Error: at least one selector is required\n", + name: "Missing selectors", + expErrPretty: "Error: at least one selector is required\n", + expErrJSON: "Error: at least one selector is required\n", + }, + { + name: "Missing parent SPIFFE ID", + args: []string{"-selector", "unix:uid:1"}, + expErrPretty: "Error: a parent ID is required if the node flag is not set\n", + expErrJSON: "Error: a parent ID is required if the node flag is not set\n", }, { - name: "Missing parent SPIFFE ID", - args: []string{"-selector", "unix:uid:1"}, - expErr: "Error: a parent ID is required if the node flag is not set\n", + name: "Missing SPIFFE ID", + args: []string{"-selector", "unix:uid:1", "-parentID", "spiffe://example.org/parent"}, + expErrPretty: "Error: a SPIFFE ID is required\n", + expErrJSON: "Error: a SPIFFE ID is required\n", }, { - name: "Missing SPIFFE ID", - args: []string{"-selector", "unix:uid:1", "-parentID", "spiffe://example.org/parent"}, - expErr: "Error: a SPIFFE ID is required\n", + name: "Wrong selectors", + args: []string{"-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload"}, + expErrPretty: "Error: selector \"unix\" must be formatted as type:value\n", + expErrJSON: "Error: selector \"unix\" must be formatted as type:value\n", }, { - name: "Wrong selectors", - args: []string{"-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload"}, - expErr: "Error: selector \"unix\" must be formatted as type:value\n", + name: "Negative TTL", + args: []string{"-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "-10"}, + expErrPretty: "Error: a positive TTL is required\n", + expErrJSON: "Error: a positive TTL is required\n", }, { - name: "Negative TTL", - args: []string{"-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "-10"}, - expErr: "Error: a positive TTL is required\n", + name: "Invalid TTL and X509SvidTtl", + args: []string{"-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "10", "-x509SVIDTTL", "20"}, + expErrPretty: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + expErrJSON: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", }, { - name: "Federated node entries", - args: []string{"-selector", "unix", "-spiffeID", "spiffe://example.org/workload", "-node", "-federatesWith", "spiffe://another.org"}, - expErr: "Error: node entries can not federate\n", + name: "Invalid TTL and JwtSvidTtl", + args: []string{"-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "10", "-jwtSVIDTTL", "20"}, + expErrPretty: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + expErrJSON: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + }, + { + name: "Invalid TTL and both X509SvidTtl and JwtSvidTtl", + args: []string{"-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "10", "-x509SVIDTTL", "20", "-jwtSVIDTTL", "30"}, + expErrPretty: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + expErrJSON: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + }, + { + name: "Federated node entries", + args: []string{"-selector", "unix", "-spiffeID", "spiffe://example.org/workload", "-node", "-federatesWith", "spiffe://another.org"}, + expErrPretty: "Error: node entries can not federate\n", + expErrJSON: "Error: node entries can not federate\n", }, { name: "Server error", @@ -162,8 +219,9 @@ func TestCreate(t *testing.T) { Selectors: []*types.Selector{{Type: "unix", Value: "uid:1"}}, }, }}, - serverErr: errors.New("server-error"), - expErr: "Error: rpc error: code = Unknown desc = server-error\n", + serverErr: errors.New("server-error"), + expErrPretty: "Error: rpc error: code = Unknown desc = server-error\n", + expErrJSON: "Error: rpc error: code = Unknown desc = server-error\n", }, { name: "Create succeeds using command line arguments", @@ -172,7 +230,8 @@ func TestCreate(t *testing.T) { "-parentID", "spiffe://example.org/parent", "-selector", "zebra:zebra:2000", "-selector", "alpha:alpha:2000", - "-ttl", "60", + "-x509SVIDTTL", "60", + "-jwtSVIDTTL", "30", "-federatesWith", "spiffe://domaina.test", "-federatesWith", "spiffe://domainb.test", "-admin", @@ -191,7 +250,8 @@ func TestCreate(t *testing.T) { {Type: "zebra", Value: "zebra:2000"}, {Type: "alpha", Value: "alpha:2000"}, }, - Ttl: 60, + X509SvidTtl: 60, + JwtSvidTtl: 30, FederatesWith: []string{"spiffe://domaina.test", "spiffe://domainb.test"}, Admin: true, ExpiresAt: 1552410266, @@ -202,12 +262,116 @@ func TestCreate(t *testing.T) { }, }, fakeResp: fakeRespOKFromCmd, - expOut: fmt.Sprintf(`Entry ID : entry-id + expOutPretty: fmt.Sprintf(`Entry ID : entry-id +SPIFFE ID : spiffe://example.org/workload +Parent ID : spiffe://example.org/parent +Revision : 0 +Downstream : true +X509-SVID TTL : 60 +JWT-SVID TTL : 30 +Expiration time : %s +Selector : zebra:zebra:2000 +Selector : alpha:alpha:2000 +FederatesWith : spiffe://domaina.test +FederatesWith : spiffe://domainb.test +DNS name : unu1000 +DNS name : ung1000 +Admin : true +StoreSvid : true + +`, time.Unix(1552410266, 0).UTC()), + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": { + "id": "entry-id", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/workload" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/parent" + }, + "selectors": [ + { + "type": "zebra", + "value": "zebra:2000" + }, + { + "type": "alpha", + "value": "alpha:2000" + } + ], + "x509_svid_ttl": 60, + "federates_with": [ + "spiffe://domaina.test", + "spiffe://domainb.test" + ], + "admin": true, + "downstream": true, + "expires_at": "1552410266", + "dns_names": [ + "unu1000", + "ung1000" + ], + "revision_number": "0", + "store_svid": true, + "jwt_svid_ttl": 30 + } + } + ] +} +`, + }, + { + name: "Create succeeds using deprecated command line arguments", + args: []string{ + "-spiffeID", "spiffe://example.org/workload", + "-parentID", "spiffe://example.org/parent", + "-selector", "zebra:zebra:2000", + "-selector", "alpha:alpha:2000", + "-ttl", "60", + "-federatesWith", "spiffe://domaina.test", + "-federatesWith", "spiffe://domainb.test", + "-admin", + "-entryExpiry", "1552410266", + "-dns", "unu1000", + "-dns", "ung1000", + "-downstream", + "-storeSVID", + }, + expReq: &entryv1.BatchCreateEntryRequest{ + Entries: []*types.Entry{ + { + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/parent"}, + Selectors: []*types.Selector{ + {Type: "zebra", Value: "zebra:2000"}, + {Type: "alpha", Value: "alpha:2000"}, + }, + X509SvidTtl: 60, + FederatesWith: []string{"spiffe://domaina.test", "spiffe://domainb.test"}, + Admin: true, + ExpiresAt: 1552410266, + DnsNames: []string{"unu1000", "ung1000"}, + Downstream: true, + StoreSvid: true, + }, + }, + }, + fakeResp: fakeRespOKFromCmd2, + expOutPretty: fmt.Sprintf(`Entry ID : entry-id SPIFFE ID : spiffe://example.org/workload Parent ID : spiffe://example.org/parent Revision : 0 Downstream : true -TTL : 60 +X509-SVID TTL : 60 +JWT-SVID TTL : default Expiration time : %s Selector : zebra:zebra:2000 Selector : alpha:alpha:2000 @@ -219,6 +383,52 @@ Admin : true StoreSvid : true `, time.Unix(1552410266, 0).UTC()), + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": { + "id": "entry-id", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/workload" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/parent" + }, + "selectors": [ + { + "type": "zebra", + "value": "zebra:2000" + }, + { + "type": "alpha", + "value": "alpha:2000" + } + ], + "x509_svid_ttl": 60, + "federates_with": [ + "spiffe://domaina.test", + "spiffe://domainb.test" + ], + "admin": true, + "downstream": true, + "expires_at": "1552410266", + "dns_names": [ + "unu1000", + "ung1000" + ], + "revision_number": "0", + "store_svid": true, + "jwt_svid_ttl": 0 + } + } + ] +}`, }, { name: "Create succeeds using data file", @@ -228,17 +438,19 @@ StoreSvid : true expReq: &entryv1.BatchCreateEntryRequest{ Entries: []*types.Entry{ { - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, - Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, - Ttl: 200, - Admin: true, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, + Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, + X509SvidTtl: 200, + JwtSvidTtl: 30, + Admin: true, }, { - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, - Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, - Ttl: 200, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, + Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, + X509SvidTtl: 200, + JwtSvidTtl: 30, }, { SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/storesvid"}, @@ -247,17 +459,19 @@ StoreSvid : true {Type: "type", Value: "key1:value"}, {Type: "type", Value: "key2:value"}, }, - Ttl: 200, - StoreSvid: true, + X509SvidTtl: 200, + JwtSvidTtl: 30, + StoreSvid: true, }, }, }, fakeResp: fakeRespOKFromFile, - expOut: `Entry ID : entry-id-1 + expOutPretty: `Entry ID : entry-id-1 SPIFFE ID : spiffe://example.org/Blog Parent ID : spiffe://example.org/spire/agent/join_token/TokenBlog Revision : 0 -TTL : 200 +X509-SVID TTL : 200 +JWT-SVID TTL : 30 Selector : unix:uid:1111 Admin : true @@ -265,19 +479,125 @@ Entry ID : entry-id-2 SPIFFE ID : spiffe://example.org/Database Parent ID : spiffe://example.org/spire/agent/join_token/TokenDatabase Revision : 0 -TTL : 200 +X509-SVID TTL : 200 +JWT-SVID TTL : 30 Selector : unix:uid:1111 Entry ID : entry-id-3 SPIFFE ID : spiffe://example.org/storesvid Parent ID : spiffe://example.org/spire/agent/join_token/TokenDatabase Revision : 0 -TTL : 200 +X509-SVID TTL : 200 +JWT-SVID TTL : 30 Selector : type:key1:value Selector : type:key2:value StoreSvid : true `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": { + "id": "entry-id-1", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/Blog" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/spire/agent/join_token/TokenBlog" + }, + "selectors": [ + { + "type": "unix", + "value": "uid:1111" + } + ], + "x509_svid_ttl": 200, + "federates_with": [], + "admin": true, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 30 + } + }, + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": { + "id": "entry-id-2", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/Database" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/spire/agent/join_token/TokenDatabase" + }, + "selectors": [ + { + "type": "unix", + "value": "uid:1111" + } + ], + "x509_svid_ttl": 200, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 30 + } + }, + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": { + "id": "entry-id-3", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/storesvid" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/spire/agent/join_token/TokenDatabase" + }, + "selectors": [ + { + "type": "type", + "value": "key1:value" + }, + { + "type": "type", + "value": "key2:value" + } + ], + "x509_svid_ttl": 200, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": true, + "jwt_svid_ttl": 30 + } + } + ] +}`, }, { name: "Entry already exist", @@ -290,34 +610,79 @@ StoreSvid : true }, }}, fakeResp: fakeRespErr, - expErr: `Failed to create the following entry (code: AlreadyExists, msg: "similar entry already exists"): + expErrPretty: `Failed to create the following entry (code: AlreadyExists, msg: "similar entry already exists"): Entry ID : (none) SPIFFE ID : spiffe://example.org/already-exist Parent ID : spiffe://example.org/spire/server Revision : 0 -TTL : default +X509-SVID TTL : default +JWT-SVID TTL : default Selector : unix:uid:1 Error: failed to create one or more entries `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 6, + "message": "similar entry already exists" + }, + "entry": { + "id": "", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/already-exist" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/spire/server" + }, + "selectors": [ + { + "type": "unix", + "value": "uid:1" + } + ], + "x509_svid_ttl": 0, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 0 + } + } + ] +}`, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newCreateCommand) - test.server.err = tt.serverErr - test.server.expBatchCreateEntryReq = tt.expReq - test.server.batchCreateEntryResp = tt.fakeResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newCreateCommand) + test.server.err = tt.serverErr + test.server.expBatchCreateEntryReq = tt.expReq + test.server.batchCreateEntryResp = tt.fakeResp + args := tt.args + args = append(args, "-output", format) - rc := test.client.Run(test.args(tt.args...)) - if tt.expErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expErr, test.stderr.String()) - return - } + rc := test.client.Run(test.args(args...)) - require.Equal(t, 0, rc) - require.Equal(t, tt.expOut, test.stdout.String()) - }) + if tt.expErrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrJSON, test.stderr.String()) + return + } + if tt.expErrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrPretty, test.stderr.String()) + return + } + require.Equal(t, 0, rc) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expOutPretty, tt.expOutJSON) + }) + } } } diff --git a/cmd/spire-server/cli/entry/delete.go b/cmd/spire-server/cli/entry/delete.go index f7c1051876..03e3d711fb 100644 --- a/cmd/spire-server/cli/entry/delete.go +++ b/cmd/spire-server/cli/entry/delete.go @@ -8,7 +8,8 @@ import ( "github.com/mitchellh/cli" entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "google.golang.org/grpc/codes" "golang.org/x/net/context" @@ -16,16 +17,18 @@ import ( // NewDeleteCommand creates a new "delete" subcommand for "entry" command. func NewDeleteCommand() cli.Command { - return newDeleteCommand(common_cli.DefaultEnv) + return newDeleteCommand(commoncli.DefaultEnv) } -func newDeleteCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(deleteCommand)) +func newDeleteCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &deleteCommand{env: env}) } type deleteCommand struct { // ID of the record to delete entryID string + env *commoncli.Env + printer cliprinter.Printer } func (*deleteCommand) Name() string { @@ -38,9 +41,10 @@ func (*deleteCommand) Synopsis() string { func (c *deleteCommand) AppendFlags(f *flag.FlagSet) { f.StringVar(&c.entryID, "entryID", "", "The Registration Entry ID of the record to delete") + cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, c.prettyPrintDelete) } -func (c *deleteCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *deleteCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if err := c.validate(); err != nil { return err } @@ -51,14 +55,7 @@ func (c *deleteCommand) Run(ctx context.Context, env *common_cli.Env, serverClie return err } - sts := resp.Results[0].Status - switch sts.Code { - case int32(codes.OK): - env.Printf("Deleted entry with ID: %s\n", c.entryID) - return nil - default: - return fmt.Errorf("failed to delete entry: %s", sts.Message) - } + return c.printer.PrintProto(resp) } // Perform basic validation. @@ -69,3 +66,19 @@ func (c *deleteCommand) validate() error { return nil } + +func (c *deleteCommand) prettyPrintDelete(env *commoncli.Env, results ...interface{}) error { + deleteResp, ok := results[0].(*entryv1.BatchDeleteEntryResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + + sts := deleteResp.Results[0].Status + switch sts.Code { + case int32(codes.OK): + env.Printf("Deleted entry with ID: %s\n", c.entryID) + return nil + default: + return fmt.Errorf("failed to delete entry: %s", sts.Message) + } +} diff --git a/cmd/spire-server/cli/entry/delete_test.go b/cmd/spire-server/cli/entry/delete_test.go index 77853ce788..c721abef85 100644 --- a/cmd/spire-server/cli/entry/delete_test.go +++ b/cmd/spire-server/cli/entry/delete_test.go @@ -2,11 +2,11 @@ package entry import ( "errors" + "fmt" "testing" entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" ) @@ -15,9 +15,7 @@ func TestDeleteHelp(t *testing.T) { test := setupTest(t, newDeleteCommand) test.client.Help() - require.Equal(t, `Usage of entry delete: - -entryID string - The Registration Entry ID of the record to delete`+common.AddrUsage, test.stderr.String()) + require.Equal(t, deleteUsage, test.stderr.String()) } func TestDeleteSynopsis(t *testing.T) { @@ -58,51 +56,65 @@ func TestDelete(t *testing.T) { fakeResp *entryv1.BatchDeleteEntryResponse serverErr error - expOut string - expErr string + expOutPretty string + expOutJSON string + expErrPretty string + expErrJSON string }{ { - name: "Empty entry ID", - expErr: "Error: an entry ID is required\n", + name: "Empty entry ID", + expErrPretty: "Error: an entry ID is required\n", + expErrJSON: "Error: an entry ID is required\n", }, { - name: "Entry not found", - args: []string{"-entryID", "entry-id"}, - expReq: &entryv1.BatchDeleteEntryRequest{Ids: []string{"entry-id"}}, - fakeResp: fakeRespErr, - expErr: "Error: failed to delete entry: entry not found\n", + name: "Entry not found", + args: []string{"-entryID", "entry-id"}, + expReq: &entryv1.BatchDeleteEntryRequest{Ids: []string{"entry-id"}}, + fakeResp: fakeRespErr, + expErrPretty: "Error: failed to delete entry: entry not found\n", + expOutJSON: `{"results":[{"status":{"code":5,"message":"entry not found"},"id":"entry-id"}]}`, }, { - name: "Server error", - args: []string{"-entryID", "entry-id"}, - expReq: &entryv1.BatchDeleteEntryRequest{Ids: []string{"entry-id"}}, - serverErr: errors.New("server-error"), - expErr: "Error: rpc error: code = Unknown desc = server-error\n", + name: "Server error", + args: []string{"-entryID", "entry-id"}, + expReq: &entryv1.BatchDeleteEntryRequest{Ids: []string{"entry-id"}}, + serverErr: errors.New("server-error"), + expErrPretty: "Error: rpc error: code = Unknown desc = server-error\n", + expErrJSON: "Error: rpc error: code = Unknown desc = server-error\n", }, { - name: "Delete succeeds", - args: []string{"-entryID", "entry-id"}, - expReq: &entryv1.BatchDeleteEntryRequest{Ids: []string{"entry-id"}}, - fakeResp: fakeRespOK, - expOut: "Deleted entry with ID: entry-id\n", + name: "Delete succeeds", + args: []string{"-entryID", "entry-id"}, + expReq: &entryv1.BatchDeleteEntryRequest{Ids: []string{"entry-id"}}, + fakeResp: fakeRespOK, + expOutPretty: "Deleted entry with ID: entry-id\n", + expOutJSON: `{"results":[{"status":{"code":0,"message":"OK"},"id":"entry-id"}]}`, }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newDeleteCommand) - test.server.err = tt.serverErr - test.server.expBatchDeleteEntryReq = tt.expReq - test.server.batchDeleteEntryResp = tt.fakeResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newDeleteCommand) + test.server.err = tt.serverErr + test.server.expBatchDeleteEntryReq = tt.expReq + test.server.batchDeleteEntryResp = tt.fakeResp + args := tt.args + args = append(args, "-output", format) - rc := test.client.Run(test.args(tt.args...)) - if tt.expErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expErr, test.stderr.String()) - return - } + rc := test.client.Run(test.args(args...)) - require.Equal(t, 0, rc) - require.Equal(t, tt.expOut, test.stdout.String()) - }) + if tt.expErrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrJSON, test.stderr.String()) + return + } + if tt.expErrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrPretty, test.stderr.String()) + return + } + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expOutPretty, tt.expOutJSON) + require.Equal(t, 0, rc) + }) + } } } diff --git a/cmd/spire-server/cli/entry/show.go b/cmd/spire-server/cli/entry/show.go index aa55da30cf..284e05dd92 100644 --- a/cmd/spire-server/cli/entry/show.go +++ b/cmd/spire-server/cli/entry/show.go @@ -9,7 +9,8 @@ import ( entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" commonutil "github.com/spiffe/spire/pkg/common/util" "golang.org/x/net/context" @@ -19,11 +20,11 @@ const listEntriesRequestPageSize = 500 // NewShowCommand creates a new "show" subcommand for "entry" command. func NewShowCommand() cli.Command { - return newShowCommand(common_cli.DefaultEnv) + return newShowCommand(commoncli.DefaultEnv) } -func newShowCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(showCommand)) +func newShowCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &showCommand{env: env}) } type showCommand struct { @@ -51,6 +52,10 @@ type showCommand struct { // Match used when filtering by selectors matchSelectorsOn string + + printer cliprinter.Printer + + env *commoncli.Env } func (c *showCommand) Name() string { @@ -70,23 +75,23 @@ func (c *showCommand) AppendFlags(f *flag.FlagSet) { f.Var(&c.federatesWith, "federatesWith", "SPIFFE ID of a trust domain an entry is federate with. Can be used more than once") f.StringVar(&c.matchFederatesWithOn, "matchFederatesWithOn", "superset", "The match mode used when filtering by federates with. Options: exact, any, superset and subset") f.StringVar(&c.matchSelectorsOn, "matchSelectorsOn", "superset", "The match mode used when filtering by selectors. Options: exact, any, superset and subset") + cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, prettyPrintShow) } // Run executes all logic associated with a single invocation of the // `spire-server entry show` CLI command -func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *showCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if err := c.validate(); err != nil { return err } - entries, err := c.fetchEntries(ctx, serverClient.NewEntryClient()) + resp, err := c.fetchEntries(ctx, serverClient.NewEntryClient()) if err != nil { return err } - commonutil.SortTypesEntries(entries) - printEntries(entries, env) - return nil + commonutil.SortTypesEntries(resp.Entries) + return c.printer.PrintProto(resp) } // validate ensures that the values in showCommand are valid @@ -101,14 +106,16 @@ func (c *showCommand) validate() error { return nil } -func (c *showCommand) fetchEntries(ctx context.Context, client entryv1.EntryClient) ([]*types.Entry, error) { +func (c *showCommand) fetchEntries(ctx context.Context, client entryv1.EntryClient) (*entryv1.ListEntriesResponse, error) { + listResp := &entryv1.ListEntriesResponse{} // If an Entry ID was specified, look it up directly if c.entryID != "" { entry, err := c.fetchByEntryID(ctx, c.entryID, client) if err != nil { return nil, fmt.Errorf("error fetching entry ID %s: %w", c.entryID, err) } - return []*types.Entry{entry}, nil + listResp.Entries = append(listResp.Entries, entry) + return listResp, nil } filter := &entryv1.ListEntriesRequest_Filter{} @@ -161,7 +168,6 @@ func (c *showCommand) fetchEntries(ctx context.Context, client entryv1.EntryClie } pageToken := "" - var entries []*types.Entry for { resp, err := client.ListEntries(ctx, &entryv1.ListEntriesRequest{ @@ -172,13 +178,13 @@ func (c *showCommand) fetchEntries(ctx context.Context, client entryv1.EntryClie if err != nil { return nil, fmt.Errorf("error fetching entries: %w", err) } - entries = append(entries, resp.Entries...) + listResp.Entries = append(listResp.Entries, resp.Entries...) if pageToken = resp.NextPageToken; pageToken == "" { break } } - return entries, nil + return listResp, nil } // fetchByEntryID uses the configured EntryID to fetch the appropriate registration entry @@ -191,7 +197,7 @@ func (c *showCommand) fetchByEntryID(ctx context.Context, id string, client entr return entry, nil } -func printEntries(entries []*types.Entry, env *common_cli.Env) { +func printEntries(entries []*types.Entry, env *commoncli.Env) { msg := fmt.Sprintf("Found %v ", len(entries)) msg = util.Pluralizer(msg, "entry", "entries", len(entries)) @@ -230,3 +236,12 @@ func parseToFederatesWithMatch(match string) (types.FederatesWithMatch_MatchBeha return types.FederatesWithMatch_MATCH_SUPERSET, fmt.Errorf("match behavior %q unknown", match) } } + +func prettyPrintShow(env *commoncli.Env, results ...interface{}) error { + listResp, ok := results[0].(*entryv1.ListEntriesResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + printEntries(listResp.Entries, env) + return nil +} diff --git a/cmd/spire-server/cli/entry/show_test.go b/cmd/spire-server/cli/entry/show_test.go index 573f6e3d5f..272997e42c 100644 --- a/cmd/spire-server/cli/entry/show_test.go +++ b/cmd/spire-server/cli/entry/show_test.go @@ -53,8 +53,9 @@ func TestShow(t *testing.T) { serverErr error - expOut string - expErr string + expOutPretty string + expOutJSON string + expErr string }{ { name: "List all entries (empty filter)", @@ -63,19 +64,26 @@ func TestShow(t *testing.T) { Filter: &entryv1.ListEntriesRequest_Filter{}, }, fakeListResp: fakeRespAll, - expOut: fmt.Sprintf("Found 4 entries\n%s%s%s%s", - getPrintedEntry(1), - getPrintedEntry(2), - getPrintedEntry(0), - getPrintedEntry(3), + expOutPretty: fmt.Sprintf("Found 4 entries\n%s%s%s%s", + getPrettyPrintedEntry(1), + getPrettyPrintedEntry(2), + getPrettyPrintedEntry(0), + getPrettyPrintedEntry(3), + ), + expOutJSON: fmt.Sprintf(`{"entries": [%s,%s,%s,%s],"next_page_token": ""}`, + getJSONPrintedEntry(1), + getJSONPrintedEntry(2), + getJSONPrintedEntry(0), + getJSONPrintedEntry(3), ), }, { - name: "List by entry ID", - args: []string{"-entryID", getEntries(1)[0].Id}, - expGetReq: &entryv1.GetEntryRequest{Id: getEntries(1)[0].Id}, - fakeGetResp: getEntries(1)[0], - expOut: fmt.Sprintf("Found 1 entry\n%s", getPrintedEntry(0)), + name: "List by entry ID", + args: []string{"-entryID", getEntries(1)[0].Id}, + expGetReq: &entryv1.GetEntryRequest{Id: getEntries(1)[0].Id}, + fakeGetResp: getEntries(1)[0], + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", getPrettyPrintedEntry(0)), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(0)), }, { name: "List by entry ID not found", @@ -99,10 +107,11 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespFather, - expOut: fmt.Sprintf("Found 2 entries\n%s%s", - getPrintedEntry(1), - getPrintedEntry(0), + expOutPretty: fmt.Sprintf("Found 2 entries\n%s%s", + getPrettyPrintedEntry(1), + getPrettyPrintedEntry(0), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s,%s],"next_page_token": ""}`, getJSONPrintedEntry(1), getJSONPrintedEntry(0)), }, { name: "List by parent ID using invalid ID", @@ -119,10 +128,11 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespDaughter, - expOut: fmt.Sprintf("Found 2 entries\n%s%s", - getPrintedEntry(1), - getPrintedEntry(2), + expOutPretty: fmt.Sprintf("Found 2 entries\n%s%s", + getPrettyPrintedEntry(1), + getPrettyPrintedEntry(2), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s, %s],"next_page_token": ""}`, getJSONPrintedEntry(1), getJSONPrintedEntry(2)), }, { name: "List by SPIFFE ID using invalid ID", @@ -145,9 +155,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespFatherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(1), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(1), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(1)), }, { name: "List by selectors: exact matcher", @@ -165,9 +176,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespFatherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(1), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(1), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(1)), }, { name: "List by selectors: superset matcher", @@ -185,9 +197,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespFatherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(1), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(1), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(1)), }, { name: "List by selectors: subset matcher", @@ -205,9 +218,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespFatherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(1), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(1), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(1)), }, { name: "List by selectors: Any matcher", @@ -225,9 +239,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespFatherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(1), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(1), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(1)), }, { name: "List by selectors: Invalid matcher", @@ -264,9 +279,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespMotherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(2), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(2), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(2)), }, { name: "List by Federates With: exact matcher", @@ -281,9 +297,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespMotherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(2), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(2), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(2)), }, { name: "List by Federates With: Any matcher", @@ -298,9 +315,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespMotherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(2), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(2), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(2)), }, { name: "List by Federates With: superset matcher", @@ -315,9 +333,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespMotherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(2), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(2), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(2)), }, { name: "List by Federates With: subset matcher", @@ -332,9 +351,10 @@ func TestShow(t *testing.T) { }, }, fakeListResp: fakeRespMotherDaughter, - expOut: fmt.Sprintf("Found 1 entry\n%s", - getPrintedEntry(2), + expOutPretty: fmt.Sprintf("Found 1 entry\n%s", + getPrettyPrintedEntry(2), ), + expOutJSON: fmt.Sprintf(`{"entries": [%s],"next_page_token": ""}`, getJSONPrintedEntry(2)), }, { name: "List by Federates With: Invalid matcher", @@ -342,25 +362,27 @@ func TestShow(t *testing.T) { expErr: "Error: match behavior \"NO-MATCHER\" unknown\n", }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newShowCommand) - test.server.err = tt.serverErr - test.server.expListEntriesReq = tt.expListReq - test.server.listEntriesResp = tt.fakeListResp - test.server.expGetEntryReq = tt.expGetReq - test.server.getEntryResp = tt.fakeGetResp - - rc := test.client.Run(test.args(tt.args...)) - if tt.expErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expErr, test.stderr.String()) - return - } + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newShowCommand) + test.server.err = tt.serverErr + test.server.expListEntriesReq = tt.expListReq + test.server.listEntriesResp = tt.fakeListResp + test.server.expGetEntryReq = tt.expGetReq + test.server.getEntryResp = tt.fakeGetResp + args := tt.args + args = append(args, "-output", format) - require.Equal(t, 0, rc) - require.Equal(t, tt.expOut, test.stdout.String()) - }) + rc := test.client.Run(test.args(args...)) + if tt.expErr != "" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErr, test.stderr.String()) + return + } + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expOutPretty, tt.expOutJSON) + require.Equal(t, 0, rc) + }) + } } } @@ -408,14 +430,15 @@ func getEntries(count int) []*types.Entry { return e } -func getPrintedEntry(idx int) string { +func getPrettyPrintedEntry(idx int) string { switch idx { case 0: return `Entry ID : 00000000-0000-0000-0000-000000000000 SPIFFE ID : spiffe://example.org/son Parent ID : spiffe://example.org/father Revision : 0 -TTL : default +X509-SVID TTL : default +JWT-SVID TTL : default Selector : foo:bar ` @@ -424,7 +447,8 @@ Selector : foo:bar SPIFFE ID : spiffe://example.org/daughter Parent ID : spiffe://example.org/father Revision : 0 -TTL : default +X509-SVID TTL : default +JWT-SVID TTL : default Selector : bar:baz Selector : foo:bar @@ -434,7 +458,8 @@ Selector : foo:bar SPIFFE ID : spiffe://example.org/daughter Parent ID : spiffe://example.org/mother Revision : 0 -TTL : default +X509-SVID TTL : default +JWT-SVID TTL : default Selector : bar:baz Selector : baz:bat FederatesWith : spiffe://domain.test @@ -445,7 +470,8 @@ FederatesWith : spiffe://domain.test SPIFFE ID : spiffe://example.org/son Parent ID : spiffe://example.org/mother Revision : 0 -TTL : default +X509-SVID TTL : default +JWT-SVID TTL : default Expiration time : %s Selector : baz:bat @@ -454,3 +480,128 @@ Selector : baz:bat return "index should be lower than 4" } } + +func getJSONPrintedEntry(idx int) string { + switch idx { + case 0: + return `{ + "id": "00000000-0000-0000-0000-000000000000", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/son" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/father" + }, + "selectors": [ + { + "type": "foo", + "value": "bar" + } + ], + "x509_svid_ttl": 0, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 0 + }` + case 1: + return `{ + "id": "00000000-0000-0000-0000-000000000001", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/daughter" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/father" + }, + "selectors": [ + { + "type": "bar", + "value": "baz" + }, + { + "type": "foo", + "value": "bar" + } + ], + "x509_svid_ttl": 0, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 0 + }` + case 2: + return `{ + "id": "00000000-0000-0000-0000-000000000002", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/daughter" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/mother" + }, + "selectors": [ + { + "type": "bar", + "value": "baz" + }, + { + "type": "baz", + "value": "bat" + } + ], + "x509_svid_ttl": 0, + "federates_with": [ + "spiffe://domain.test" + ], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 0 + }` + case 3: + return `{ + "id": "00000000-0000-0000-0000-000000000003", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/son" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/mother" + }, + "selectors": [ + { + "type": "baz", + "value": "bat" + } + ], + "x509_svid_ttl": 0, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "1552410266", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 0 + }` + default: + return "index should be lower than 4" + } +} diff --git a/cmd/spire-server/cli/entry/update.go b/cmd/spire-server/cli/entry/update.go index 7684956937..f4faa04599 100644 --- a/cmd/spire-server/cli/entry/update.go +++ b/cmd/spire-server/cli/entry/update.go @@ -8,7 +8,8 @@ import ( entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "google.golang.org/grpc/codes" "golang.org/x/net/context" @@ -16,11 +17,11 @@ import ( // NewUpdateCommand creates a new "update" subcommand for "entry" command. func NewUpdateCommand() cli.Command { - return newUpdateCommand(common_cli.DefaultEnv) + return newUpdateCommand(commoncli.DefaultEnv) } -func newUpdateCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(updateCommand)) +func newUpdateCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &updateCommand{env: env}) } type updateCommand struct { @@ -47,6 +48,12 @@ type updateCommand struct { // TTL for certificates issued to this workload ttl int + // TTL for x509 SVIDs issued to this workload + x509SvidTTL int + + // TTL for JWT SVIDs issued to this workload + jwtSvidTTL int + // List of SPIFFE IDs of trust domains the registration entry is federated with federatesWith StringsFlag @@ -61,6 +68,10 @@ type updateCommand struct { // storeSVID determines if the issued SVID must be stored through an SVIDStore plugin storeSVID bool + + printer cliprinter.Printer + + env *commoncli.Env } func (*updateCommand) Name() string { @@ -75,7 +86,9 @@ func (c *updateCommand) AppendFlags(f *flag.FlagSet) { f.StringVar(&c.entryID, "entryID", "", "The Registration Entry ID of the record to update") f.StringVar(&c.parentID, "parentID", "", "The SPIFFE ID of this record's parent") f.StringVar(&c.spiffeID, "spiffeID", "", "The SPIFFE ID that this record represents") - f.IntVar(&c.ttl, "ttl", 0, "The lifetime, in seconds, for SVIDs issued based on this registration entry") + f.IntVar(&c.ttl, "ttl", 0, "The lifetime, in seconds, for SVIDs issued based on this registration entry. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version") + f.IntVar(&c.x509SvidTTL, "x509SVIDTTL", 0, "The lifetime, in seconds, for x509-SVIDs issued based on this registration entry. Overrides ttl flag") + f.IntVar(&c.jwtSvidTTL, "jwtSVIDTTL", 0, "The lifetime, in seconds, for JWT-SVIDs issued based on this registration entry. Overrides ttl flag") f.StringVar(&c.path, "data", "", "Path to a file containing registration JSON (optional). If set to '-', read the JSON from stdin.") f.Var(&c.selectors, "selector", "A colon-delimited type:value selector. Can be used more than once") f.Var(&c.federatesWith, "federatesWith", "SPIFFE ID of a trust domain to federate with. Can be used more than once") @@ -84,9 +97,10 @@ func (c *updateCommand) AppendFlags(f *flag.FlagSet) { f.BoolVar(&c.storeSVID, "storeSVID", false, "A boolean value that, when set, indicates that the resulting issued SVID from this entry must be stored through an SVIDStore plugin") f.Int64Var(&c.entryExpiry, "entryExpiry", 0, "An expiry, from epoch in seconds, for the resulting registration entry to be pruned") f.Var(&c.dnsNames, "dns", "A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once") + cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, prettyPrintUpdate) } -func (c *updateCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *updateCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if err := c.validate(); err != nil { return err } @@ -102,29 +116,12 @@ func (c *updateCommand) Run(ctx context.Context, env *common_cli.Env, serverClie return err } - succeeded, failed, err := updateEntries(ctx, serverClient.NewEntryClient(), entries) + resp, err := updateEntries(ctx, serverClient.NewEntryClient(), entries) if err != nil { return err } - // Print entries that succeeded to be updated - for _, e := range succeeded { - printEntry(e.Entry, env.Printf) - } - - // Print entries that failed to be updated - for _, r := range failed { - env.ErrPrintf("Failed to update the following entry (code: %s, msg: %q):\n", - codes.Code(r.Status.Code), - r.Status.Message) - printEntry(r.Entry, env.ErrPrintf) - } - - if len(failed) > 0 { - return errors.New("failed to update one or more entries") - } - - return nil + return c.printer.PrintProto(resp) } // validate performs basic validation, even on fields that we @@ -155,6 +152,18 @@ func (c *updateCommand) validate() (err error) { return errors.New("a positive TTL is required") } + if c.x509SvidTTL < 0 { + return errors.New("a positive x509-SVID TTL is required") + } + + if c.jwtSvidTTL < 0 { + return errors.New("a positive JWT-SVID TTL is required") + } + + if c.ttl > 0 && (c.x509SvidTTL > 0 || c.jwtSvidTTL > 0) { + return errors.New("use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag") + } + return nil } @@ -170,13 +179,26 @@ func (c *updateCommand) parseConfig() ([]*types.Entry, error) { } e := &types.Entry{ - Id: c.entryID, - ParentId: parentID, - SpiffeId: spiffeID, - Ttl: int32(c.ttl), - Downstream: c.downstream, - ExpiresAt: c.entryExpiry, - DnsNames: c.dnsNames, + Id: c.entryID, + ParentId: parentID, + SpiffeId: spiffeID, + Downstream: c.downstream, + ExpiresAt: c.entryExpiry, + DnsNames: c.dnsNames, + X509SvidTtl: int32(c.x509SvidTTL), + JwtSvidTtl: int32(c.jwtSvidTTL), + } + + // c.ttl is deprecated but usable if the new c.x509Svid field is not used. + // c.ttl should not be used to set the jwtSVIDTTL value because the previous + // behavior was to have a hard-coded 5 minute JWT TTL no matter what the value + // of ttl was set to. + // validate(...) ensures that either the new fields or the deprecated field is + // used, but never a mixture. + // + // https://github.com/spiffe/spire/issues/2700 + if e.X509SvidTtl == 0 { + e.X509SvidTtl = int32(c.ttl) } selectors := []*types.Selector{} @@ -196,25 +218,56 @@ func (c *updateCommand) parseConfig() ([]*types.Entry, error) { return []*types.Entry{e}, nil } -func updateEntries(ctx context.Context, c entryv1.EntryClient, entries []*types.Entry) (succeeded, failed []*entryv1.BatchUpdateEntryResponse_Result, err error) { - resp, err := c.BatchUpdateEntry(ctx, &entryv1.BatchUpdateEntryRequest{ +func updateEntries(ctx context.Context, c entryv1.EntryClient, entries []*types.Entry) (resp *entryv1.BatchUpdateEntryResponse, err error) { + resp, err = c.BatchUpdateEntry(ctx, &entryv1.BatchUpdateEntryRequest{ Entries: entries, }) if err != nil { - return nil, nil, err + return } for i, r := range resp.Results { + if r.Status.Code != int32(codes.OK) { + // The Entry API does not include in the results the entries that + // failed to be updated, so we populate them from the request data. + r.Entry = entries[i] + } + } + + return +} + +func prettyPrintUpdate(env *commoncli.Env, results ...interface{}) error { + var succeeded, failed []*entryv1.BatchUpdateEntryResponse_Result + updateResp, ok := results[0].(*entryv1.BatchUpdateEntryResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + + for _, r := range updateResp.Results { switch r.Status.Code { case int32(codes.OK): succeeded = append(succeeded, r) default: - // The Entry API does not include in the results the entries that - // failed to be updated, so we populate them from the request data. - r.Entry = entries[i] failed = append(failed, r) } } + // Print entries that succeeded to be updated + for _, e := range succeeded { + printEntry(e.Entry, env.Printf) + } - return succeeded, failed, nil + // Print entries that failed to be updated + for _, r := range failed { + env.ErrPrintf("Failed to update the following entry (code: %s, msg: %q):\n", + codes.Code(r.Status.Code), + r.Status.Message) + printEntry(r.Entry, env.ErrPrintf) + } + + if len(failed) > 0 { + return errors.New("failed to update one or more entries") + } + + return nil } diff --git a/cmd/spire-server/cli/entry/update_test.go b/cmd/spire-server/cli/entry/update_test.go index aafc494c45..07761a785f 100644 --- a/cmd/spire-server/cli/entry/update_test.go +++ b/cmd/spire-server/cli/entry/update_test.go @@ -15,7 +15,6 @@ import ( func TestUpdateHelp(t *testing.T) { test := setupTest(t, newUpdateCommand) test.client.Help() - require.Equal(t, updateUsage, test.stderr.String()) } @@ -25,6 +24,189 @@ func TestUpdateSynopsis(t *testing.T) { } func TestUpdate(t *testing.T) { + entry0JSON := `{ + "id": "entry-id", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/workload" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/parent" + }, + "selectors": [ + { + "type": "type", + "value": "key1:value" + }, + { + "type": "type", + "value": "key2:value" + } + ], + "x509_svid_ttl": 60, + "federates_with": [ + "spiffe://domaina.test", + "spiffe://domainb.test" + ], + "admin": false, + "downstream": false, + "expires_at": "1552410266", + "dns_names": [ + "unu1000", + "ung1000" + ], + "revision_number": "0", + "store_svid": true, + "jwt_svid_ttl":30 + }` + entry0AdminJSON := `{ + "id": "entry-id", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/workload" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/parent" + }, + "selectors": [ + { + "type": "zebra", + "value": "zebra:2000" + }, + { + "type": "alpha", + "value": "alpha:2000" + } + ], + "x509_svid_ttl": 60, + "federates_with": [ + "spiffe://domaina.test", + "spiffe://domainb.test" + ], + "admin": true, + "downstream": true, + "expires_at": "1552410266", + "dns_names": [ + "unu1000", + "ung1000" + ], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl":30 + }` + entry1JSON := `{ + "id": "entry-id-1", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/Blog" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/spire/agent/join_token/TokenBlog" + }, + "selectors": [ + { + "type": "unix", + "value": "uid:1111" + } + ], + "x509_svid_ttl": 200, + "federates_with": [], + "admin": true, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl": 300 + } + }` + entry2JSON := `{ + "id": "entry-id-2", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/Database" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/spire/agent/join_token/TokenDatabase" + }, + "selectors": [ + { + "type": "unix", + "value": "uid:1111" + } + ], + "x509_svid_ttl": 200, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "jwt_svid_ttl":300 + } + }` + entry3JSON := `{ + "id": "entry-id-3", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/Storesvid" + }, + "parent_id": { + "trust_domain": "example.org", + "path": "/spire/agent/join_token/TokenDatabase" + }, + "selectors": [ + { + "type": "type", + "value": "key1:value" + }, + { + "type": "type", + "value": "key2:value" + } + ], + "x509_svid_ttl": 200, + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": true, + "jwt_svid_ttl":300 + }` + nonExistentEntryJSON := `{ + "id": "non-existent-id", + "spiffe_id": { + "trust_domain": "example.org", + "path": "/workload" + }, + "jwt_svid_ttl": 0, + "parent_id": { + "trust_domain": "example.org", + "path": "/parent" + }, + "selectors": [ + { + "type": "unix", + "value": "uid:1" + } + ], + "federates_with": [], + "admin": false, + "downstream": false, + "expires_at": "0", + "dns_names": [], + "revision_number": "0", + "store_svid": false, + "x509_svid_ttl": 0 + }` + entry1 := &types.Entry{ Id: "entry-id", SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"}, @@ -33,7 +215,8 @@ func TestUpdate(t *testing.T) { {Type: "zebra", Value: "zebra:2000"}, {Type: "alpha", Value: "alpha:2000"}, }, - Ttl: 60, + X509SvidTtl: 60, + JwtSvidTtl: 30, FederatesWith: []string{"spiffe://domaina.test", "spiffe://domainb.test"}, Admin: true, ExpiresAt: 1552410266, @@ -49,7 +232,8 @@ func TestUpdate(t *testing.T) { {Type: "type", Value: "key1:value"}, {Type: "type", Value: "key2:value"}, }, - Ttl: 60, + X509SvidTtl: 60, + JwtSvidTtl: 30, FederatesWith: []string{"spiffe://domaina.test", "spiffe://domainb.test"}, ExpiresAt: 1552410266, DnsNames: []string{"unu1000", "ung1000"}, @@ -68,20 +252,22 @@ func TestUpdate(t *testing.T) { } entry2 := &types.Entry{ - Id: "entry-id-1", - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, - Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, - Ttl: 200, - Admin: true, + Id: "entry-id-1", + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, + Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, + X509SvidTtl: 200, + JwtSvidTtl: 300, + Admin: true, } entry3 := &types.Entry{ - Id: "entry-id-2", - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, - Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, - Ttl: 200, + Id: "entry-id-2", + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, + Selectors: []*types.Selector{{Type: "unix", Value: "uid:1111"}}, + X509SvidTtl: 200, + JwtSvidTtl: 300, } entry4 := &types.Entry{ @@ -92,8 +278,26 @@ func TestUpdate(t *testing.T) { {Type: "type", Value: "key1:value"}, {Type: "type", Value: "key2:value"}, }, - StoreSvid: true, - Ttl: 200, + StoreSvid: true, + X509SvidTtl: 200, + JwtSvidTtl: 300, + } + + entry5 := &types.Entry{ + Id: "entry-id", + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/parent"}, + Selectors: []*types.Selector{ + {Type: "zebra", Value: "zebra:2000"}, + {Type: "alpha", Value: "alpha:2000"}, + }, + X509SvidTtl: 60, + JwtSvidTtl: 0, + FederatesWith: []string{"spiffe://domaina.test", "spiffe://domainb.test"}, + Admin: true, + ExpiresAt: 1552410266, + DnsNames: []string{"unu1000", "ung1000"}, + Downstream: true, } fakeRespOKFromFile := &entryv1.BatchUpdateEntryResponse{ @@ -132,37 +336,63 @@ func TestUpdate(t *testing.T) { fakeResp *entryv1.BatchUpdateEntryResponse serverErr error - expOut string - expErr string + expOutPretty string + expOutJSON string + expErrPretty string + expErrJSON string }{ { - name: "Missing Entry ID", - expErr: "Error: entry ID is required\n", + name: "Missing Entry ID", + expErrPretty: "Error: entry ID is required\n", + expErrJSON: "Error: entry ID is required\n", + }, + { + name: "Missing selectors", + args: []string{"-entryID", "entry-id"}, + expErrPretty: "Error: at least one selector is required\n", + expErrJSON: "Error: at least one selector is required\n", + }, + { + name: "Missing parent SPIFFE ID", + args: []string{"-entryID", "entry-id", "-selector", "unix:uid:1"}, + expErrPretty: "Error: a parent ID is required\n", + expErrJSON: "Error: a parent ID is required\n", + }, + { + name: "Missing SPIFFE ID", + args: []string{"-entryID", "entry-id", "-selector", "unix:uid:1", "-parentID", "spiffe://example.org/parent"}, + expErrPretty: "Error: a SPIFFE ID is required\n", + expErrJSON: "Error: a SPIFFE ID is required\n", }, { - name: "Missing selectors", - args: []string{"-entryID", "entry-id"}, - expErr: "Error: at least one selector is required\n", + name: "Wrong selectors", + args: []string{"-entryID", "entry-id", "-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload"}, + expErrPretty: "Error: selector \"unix\" must be formatted as type:value\n", + expErrJSON: "Error: selector \"unix\" must be formatted as type:value\n", }, { - name: "Missing parent SPIFFE ID", - args: []string{"-entryID", "entry-id", "-selector", "unix:uid:1"}, - expErr: "Error: a parent ID is required\n", + name: "Negative TTL", + args: []string{"-entryID", "entry-id", "-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "-10"}, + expErrPretty: "Error: a positive TTL is required\n", + expErrJSON: "Error: a positive TTL is required\n", }, { - name: "Missing SPIFFE ID", - args: []string{"-entryID", "entry-id", "-selector", "unix:uid:1", "-parentID", "spiffe://example.org/parent"}, - expErr: "Error: a SPIFFE ID is required\n", + name: "Invalid TTL and X509SvidTtl", + args: []string{"-entryID", "entry-id", "-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "10", "-x509SVIDTTL", "20"}, + expErrPretty: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + expErrJSON: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", }, { - name: "Wrong selectors", - args: []string{"-entryID", "entry-id", "-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload"}, - expErr: "Error: selector \"unix\" must be formatted as type:value\n", + name: "Invalid TTL and JwtSvidTtl", + args: []string{"-entryID", "entry-id", "-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "10", "-jwtSVIDTTL", "20"}, + expErrPretty: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + expErrJSON: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", }, { - name: "Negative TTL", - args: []string{"-entryID", "entry-id", "-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "-10"}, - expErr: "Error: a positive TTL is required\n", + name: "Invalid TTL and both X509SvidTtl and JwtSvidTtl", + args: []string{"-entryID", "entry-id", "-selector", "unix", "-parentID", "spiffe://example.org/parent", "-spiffeID", "spiffe://example.org/workload", "-ttl", "10", "-x509SVIDTTL", "20", "-jwtSVIDTTL", "30"}, + expErrPretty: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", + expErrJSON: "Error: use x509SVIDTTL and jwtSVIDTTL flags or the deprecated ttl flag\n", }, { name: "Server error", @@ -175,8 +405,9 @@ func TestUpdate(t *testing.T) { Selectors: []*types.Selector{{Type: "unix", Value: "uid:1"}}, }, }}, - serverErr: errors.New("server-error"), - expErr: "Error: rpc error: code = Unknown desc = server-error\n", + serverErr: errors.New("server-error"), + expErrPretty: "Error: rpc error: code = Unknown desc = server-error\n", + expErrJSON: "Error: rpc error: code = Unknown desc = server-error\n", }, { name: "Update succeeds using command line arguments", @@ -186,7 +417,8 @@ func TestUpdate(t *testing.T) { "-parentID", "spiffe://example.org/parent", "-selector", "zebra:zebra:2000", "-selector", "alpha:alpha:2000", - "-ttl", "60", + "-x509SVIDTTL", "60", + "-jwtSVIDTTL", "30", "-federatesWith", "spiffe://domaina.test", "-federatesWith", "spiffe://domainb.test", "-admin", @@ -199,12 +431,13 @@ func TestUpdate(t *testing.T) { Entries: []*types.Entry{entry1}, }, fakeResp: fakeRespOKFromCmd, - expOut: fmt.Sprintf(`Entry ID : entry-id + expOutPretty: fmt.Sprintf(`Entry ID : entry-id SPIFFE ID : spiffe://example.org/workload Parent ID : spiffe://example.org/parent Revision : 0 Downstream : true -TTL : 60 +X509-SVID TTL : 60 +JWT-SVID TTL : 30 Expiration time : %s Selector : zebra:zebra:2000 Selector : alpha:alpha:2000 @@ -215,6 +448,67 @@ DNS name : ung1000 Admin : true `, time.Unix(1552410266, 0).UTC()), + expOutJSON: fmt.Sprintf(`{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": %s + } + ] +}`, entry0AdminJSON), + }, + { + name: "Update succeeds using deprecated command line arguments", + args: []string{ + "-entryID", "entry-id", + "-spiffeID", "spiffe://example.org/workload", + "-parentID", "spiffe://example.org/parent", + "-selector", "zebra:zebra:2000", + "-selector", "alpha:alpha:2000", + "-ttl", "60", + "-federatesWith", "spiffe://domaina.test", + "-federatesWith", "spiffe://domainb.test", + "-admin", + "-entryExpiry", "1552410266", + "-dns", "unu1000", + "-dns", "ung1000", + "-downstream", + }, + expReq: &entryv1.BatchUpdateEntryRequest{ + Entries: []*types.Entry{entry5}, + }, + fakeResp: fakeRespOKFromCmd, + expOutPretty: fmt.Sprintf(`Entry ID : entry-id +SPIFFE ID : spiffe://example.org/workload +Parent ID : spiffe://example.org/parent +Revision : 0 +Downstream : true +X509-SVID TTL : 60 +JWT-SVID TTL : 30 +Expiration time : %s +Selector : zebra:zebra:2000 +Selector : alpha:alpha:2000 +FederatesWith : spiffe://domaina.test +FederatesWith : spiffe://domainb.test +DNS name : unu1000 +DNS name : ung1000 +Admin : true + +`, time.Unix(1552410266, 0).UTC()), + expOutJSON: fmt.Sprintf(`{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": %s + } + ] +}`, entry0AdminJSON), }, { name: "Update succeeds using command line arguments Store Svid", @@ -224,7 +518,8 @@ Admin : true "-parentID", "spiffe://example.org/parent", "-selector", "type:key1:value", "-selector", "type:key2:value", - "-ttl", "60", + "-x509SVIDTTL", "60", + "-jwtSVIDTTL", "30", "-federatesWith", "spiffe://domaina.test", "-federatesWith", "spiffe://domainb.test", "-entryExpiry", "1552410266", @@ -246,11 +541,12 @@ Admin : true }, }, }, - expOut: fmt.Sprintf(`Entry ID : entry-id + expOutPretty: fmt.Sprintf(`Entry ID : entry-id SPIFFE ID : spiffe://example.org/workload Parent ID : spiffe://example.org/parent Revision : 0 -TTL : 60 +X509-SVID TTL : 60 +JWT-SVID TTL : 30 Expiration time : %s Selector : type:key1:value Selector : type:key2:value @@ -261,6 +557,17 @@ DNS name : ung1000 StoreSvid : true `, time.Unix(1552410266, 0).UTC()), + expOutJSON: fmt.Sprintf(`{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": %s + } + ] +}`, entry0JSON), }, { name: "Update succeeds using data file", @@ -271,11 +578,12 @@ StoreSvid : true Entries: []*types.Entry{entry2, entry3, entry4}, }, fakeResp: fakeRespOKFromFile, - expOut: `Entry ID : entry-id-1 + expOutPretty: `Entry ID : entry-id-1 SPIFFE ID : spiffe://example.org/Blog Parent ID : spiffe://example.org/spire/agent/join_token/TokenBlog Revision : 0 -TTL : 200 +X509-SVID TTL : 200 +JWT-SVID TTL : 300 Selector : unix:uid:1111 Admin : true @@ -283,19 +591,45 @@ Entry ID : entry-id-2 SPIFFE ID : spiffe://example.org/Database Parent ID : spiffe://example.org/spire/agent/join_token/TokenDatabase Revision : 0 -TTL : 200 +X509-SVID TTL : 200 +JWT-SVID TTL : 300 Selector : unix:uid:1111 Entry ID : entry-id-3 SPIFFE ID : spiffe://example.org/Storesvid Parent ID : spiffe://example.org/spire/agent/join_token/TokenDatabase Revision : 0 -TTL : 200 +X509-SVID TTL : 200 +JWT-SVID TTL : 300 Selector : type:key1:value Selector : type:key2:value StoreSvid : true `, + expOutJSON: fmt.Sprintf(` +{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": %s, + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": %s, + { + "status": { + "code": 0, + "message": "OK" + }, + "entry": %s + } + ] +}`, entry1JSON, entry2JSON, entry3JSON), }, { name: "Entry not found", @@ -309,34 +643,55 @@ StoreSvid : true }, }}, fakeResp: fakeRespErr, - expErr: `Failed to update the following entry (code: NotFound, msg: "failed to update entry: datastore-sql: record not found"): + expErrPretty: `Failed to update the following entry (code: NotFound, msg: "failed to update entry: datastore-sql: record not found"): Entry ID : non-existent-id SPIFFE ID : spiffe://example.org/workload Parent ID : spiffe://example.org/parent Revision : 0 -TTL : default +X509-SVID TTL : default +JWT-SVID TTL : default Selector : unix:uid:1 Error: failed to update one or more entries `, + expOutJSON: fmt.Sprintf(`{ + "results": [ + { + "status": { + "code": 5, + "message": "failed to update entry: datastore-sql: record not found" + }, + "entry": %s + } + ] +}`, nonExistentEntryJSON), }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newUpdateCommand) - test.server.err = tt.serverErr - test.server.expBatchUpdateEntryReq = tt.expReq - test.server.batchUpdateEntryResp = tt.fakeResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newUpdateCommand) + test.server.err = tt.serverErr + test.server.expBatchUpdateEntryReq = tt.expReq + test.server.batchUpdateEntryResp = tt.fakeResp + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(args...)) - rc := test.client.Run(test.args(tt.args...)) - if tt.expErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expErr, test.stderr.String()) - return - } + if tt.expErrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrJSON, test.stderr.String()) + return + } + if tt.expErrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrPretty, test.stderr.String()) + return + } - require.Equal(t, 0, rc) - require.Equal(t, tt.expOut, test.stdout.String()) - }) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expOutPretty, tt.expOutJSON) + require.Equal(t, 0, rc) + }) + } } } diff --git a/cmd/spire-server/cli/entry/util.go b/cmd/spire-server/cli/entry/util.go index 5d84cb68b5..c535facc83 100644 --- a/cmd/spire-server/cli/entry/util.go +++ b/cmd/spire-server/cli/entry/util.go @@ -23,10 +23,16 @@ func printEntry(e *types.Entry, printf func(string, ...interface{}) error) { _ = printf("Downstream : %t\n", e.Downstream) } - if e.Ttl == 0 { - _ = printf("TTL : default\n") + if e.X509SvidTtl == 0 { + _ = printf("X509-SVID TTL : default\n") } else { - _ = printf("TTL : %d\n", e.Ttl) + _ = printf("X509-SVID TTL : %d\n", e.X509SvidTtl) + } + + if e.JwtSvidTtl == 0 { + _ = printf("JWT-SVID TTL : default\n") + } else { + _ = printf("JWT-SVID TTL : %d\n", e.JwtSvidTtl) } if e.ExpiresAt != 0 { diff --git a/cmd/spire-server/cli/entry/util_posix_test.go b/cmd/spire-server/cli/entry/util_posix_test.go index 8052dbbd5f..f010b04de4 100644 --- a/cmd/spire-server/cli/entry/util_posix_test.go +++ b/cmd/spire-server/cli/entry/util_posix_test.go @@ -17,8 +17,12 @@ const ( An expiry, from epoch in seconds, for the resulting registration entry to be pruned -federatesWith value SPIFFE ID of a trust domain to federate with. Can be used more than once + -jwtSVIDTTL int + The lifetime, in seconds, for JWT-SVIDs issued based on this registration entry. Overrides ttl flag -node If set, this entry will be applied to matching nodes rather than workloads + -output value + Desired output format (pretty, json); default: pretty. -parentID string The SPIFFE ID of this record's parent -selector value @@ -30,7 +34,9 @@ const ( -storeSVID A boolean value that, when set, indicates that the resulting issued SVID from this entry must be stored through an SVIDStore plugin -ttl int - The lifetime, in seconds, for SVIDs issued based on this registration entry + The lifetime, in seconds, for SVIDs issued based on this registration entry. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version + -x509SVIDTTL int + The lifetime, in seconds, for x509-SVIDs issued based on this registration entry. Overrides ttl flag ` showUsage = `Usage of entry show: -downstream @@ -43,6 +49,8 @@ const ( The match mode used when filtering by federates with. Options: exact, any, superset and subset (default "superset") -matchSelectorsOn string The match mode used when filtering by selectors. Options: exact, any, superset and subset (default "superset") + -output value + Desired output format (pretty, json); default: pretty. -parentID string The Parent ID of the records to show -selector value @@ -67,6 +75,10 @@ const ( The Registration Entry ID of the record to update -federatesWith value SPIFFE ID of a trust domain to federate with. Can be used more than once + -jwtSVIDTTL int + The lifetime, in seconds, for JWT-SVIDs issued based on this registration entry. Overrides ttl flag + -output value + Desired output format (pretty, json); default: pretty. -parentID string The SPIFFE ID of this record's parent -selector value @@ -78,6 +90,22 @@ const ( -storeSVID A boolean value that, when set, indicates that the resulting issued SVID from this entry must be stored through an SVIDStore plugin -ttl int - The lifetime, in seconds, for SVIDs issued based on this registration entry + The lifetime, in seconds, for SVIDs issued based on this registration entry. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version + -x509SVIDTTL int + The lifetime, in seconds, for x509-SVIDs issued based on this registration entry. Overrides ttl flag +` + deleteUsage = `Usage of entry delete: + -entryID string + The Registration Entry ID of the record to delete + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + countUsage = `Usage of entry count: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") ` ) diff --git a/cmd/spire-server/cli/entry/util_test.go b/cmd/spire-server/cli/entry/util_test.go index 774d599acf..617bca4ad6 100644 --- a/cmd/spire-server/cli/entry/util_test.go +++ b/cmd/spire-server/cli/entry/util_test.go @@ -19,6 +19,8 @@ import ( "google.golang.org/grpc" ) +var availableFormats = []string{"pretty", "json"} + func TestParseEntryJSON(t *testing.T) { testCases := []struct { name string @@ -69,10 +71,11 @@ func TestParseEntryJSON(t *testing.T) { Value: "uid:1111", }, }, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, - Ttl: 200, - Admin: true, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Blog"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenBlog"}, + X509SvidTtl: 200, + JwtSvidTtl: 30, + Admin: true, } entry2 := &types.Entry{ Selectors: []*types.Selector{ @@ -81,9 +84,10 @@ func TestParseEntryJSON(t *testing.T) { Value: "uid:1111", }, }, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, - Ttl: 200, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/Database"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, + X509SvidTtl: 200, + JwtSvidTtl: 30, } entry3 := &types.Entry{ Selectors: []*types.Selector{ @@ -96,10 +100,11 @@ func TestParseEntryJSON(t *testing.T) { Value: "key2:value", }, }, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/storesvid"}, - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, - StoreSvid: true, - Ttl: 200, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/storesvid"}, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/join_token/TokenDatabase"}, + StoreSvid: true, + X509SvidTtl: 200, + JwtSvidTtl: 30, } expectedEntries := []*types.Entry{ @@ -250,3 +255,16 @@ func setupTest(t *testing.T, newClient func(*common_cli.Env) cli.Command) *entry return test } + +func requireOutputBasedOnFormat(t *testing.T, format, stdoutString string, expectedStdoutPretty, expectedStdoutJSON string) { + switch format { + case "pretty": + require.Contains(t, stdoutString, expectedStdoutPretty) + case "json": + if expectedStdoutJSON != "" { + require.JSONEq(t, expectedStdoutJSON, stdoutString) + } else { + require.Empty(t, stdoutString) + } + } +} diff --git a/cmd/spire-server/cli/entry/util_windows_test.go b/cmd/spire-server/cli/entry/util_windows_test.go index 2a99c42bce..0f11661160 100644 --- a/cmd/spire-server/cli/entry/util_windows_test.go +++ b/cmd/spire-server/cli/entry/util_windows_test.go @@ -17,10 +17,14 @@ const ( An expiry, from epoch in seconds, for the resulting registration entry to be pruned -federatesWith value SPIFFE ID of a trust domain to federate with. Can be used more than once + -jwtSVIDTTL int + The lifetime, in seconds, for JWT-SVIDs issued based on this registration entry. Overrides ttl flag -namedPipeName string Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") -node If set, this entry will be applied to matching nodes rather than workloads + -output value + Desired output format (pretty, json); default: pretty. -parentID string The SPIFFE ID of this record's parent -selector value @@ -30,7 +34,9 @@ const ( -storeSVID A boolean value that, when set, indicates that the resulting issued SVID from this entry must be stored through an SVIDStore plugin -ttl int - The lifetime, in seconds, for SVIDs issued based on this registration entry + The lifetime, in seconds, for SVIDs issued based on this registration entry. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version + -x509SVIDTTL int + The lifetime, in seconds, for x509-SVIDs issued based on this registration entry. Overrides ttl flag ` showUsage = `Usage of entry show: -downstream @@ -45,6 +51,8 @@ const ( The match mode used when filtering by selectors. Options: exact, any, superset and subset (default "superset") -namedPipeName string Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. -parentID string The Parent ID of the records to show -selector value @@ -67,8 +75,12 @@ const ( The Registration Entry ID of the record to update -federatesWith value SPIFFE ID of a trust domain to federate with. Can be used more than once + -jwtSVIDTTL int + The lifetime, in seconds, for JWT-SVIDs issued based on this registration entry. Overrides ttl flag -namedPipeName string Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. -parentID string The SPIFFE ID of this record's parent -selector value @@ -78,6 +90,22 @@ const ( -storeSVID A boolean value that, when set, indicates that the resulting issued SVID from this entry must be stored through an SVIDStore plugin -ttl int - The lifetime, in seconds, for SVIDs issued based on this registration entry + The lifetime, in seconds, for SVIDs issued based on this registration entry. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version + -x509SVIDTTL int + The lifetime, in seconds, for x509-SVIDs issued based on this registration entry. Overrides ttl flag +` + deleteUsage = `Usage of entry delete: + -entryID string + The Registration Entry ID of the record to delete + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + countUsage = `Usage of entry count: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. ` ) diff --git a/cmd/spire-server/cli/federation/common_test.go b/cmd/spire-server/cli/federation/common_test.go index 491874c823..e9750f5747 100644 --- a/cmd/spire-server/cli/federation/common_test.go +++ b/cmd/spire-server/cli/federation/common_test.go @@ -103,6 +103,8 @@ const ( }` ) +var availableFormats = []string{"pretty", "json"} + type cmdTest struct { stdin *bytes.Buffer stdout *bytes.Buffer @@ -239,7 +241,7 @@ func createBundle(t *testing.T, trustDomain string) (*types.Bundle, string) { td := spiffeid.RequireTrustDomainFromString(trustDomain) bundlePath := path.Join(t.TempDir(), "bundle.pem") ca := fakeserverca.New(t, td, &fakeserverca.Options{}) - require.NoError(t, pemutil.SaveCertificates(bundlePath, ca.Bundle(), 0600)) + require.NoError(t, os.WriteFile(bundlePath, pemutil.EncodeCertificates(ca.Bundle()), 0600)) return &types.Bundle{ TrustDomain: td.String(), @@ -260,3 +262,16 @@ func createJSONDataFile(t *testing.T, data string) string { require.NoError(t, os.WriteFile(jsonDataFilePath, []byte(data), 0600)) return jsonDataFilePath } + +func requireOutputBasedOnFormat(t *testing.T, format, stdoutString string, expectedStdoutPretty, expectedStdoutJSON string) { + switch format { + case "pretty": + require.Contains(t, stdoutString, expectedStdoutPretty) + case "json": + if expectedStdoutJSON != "" { + require.JSONEq(t, expectedStdoutJSON, stdoutString) + } else { + require.Empty(t, stdoutString) + } + } +} diff --git a/cmd/spire-server/cli/federation/create.go b/cmd/spire-server/cli/federation/create.go index 6b73ace6e3..1d2ff1769b 100644 --- a/cmd/spire-server/cli/federation/create.go +++ b/cmd/spire-server/cli/federation/create.go @@ -8,8 +8,10 @@ import ( "github.com/mitchellh/cli" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" + "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "google.golang.org/grpc/codes" ) @@ -20,16 +22,19 @@ const ( // NewCreateCommand creates a new "create" subcommand for "federation" command. func NewCreateCommand() cli.Command { - return newCreateCommand(common_cli.DefaultEnv) + return newCreateCommand(commoncli.DefaultEnv) } -func newCreateCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(createCommand)) +func newCreateCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &createCommand{env: env}) } type createCommand struct { - path string - config *federationRelationshipConfig + path string + config *federationRelationshipConfig + env *commoncli.Env + printer cliprinter.Printer + federationRelationships []*types.FederationRelationship } func (*createCommand) Name() string { @@ -44,13 +49,15 @@ func (c *createCommand) AppendFlags(f *flag.FlagSet) { f.StringVar(&c.path, "data", "", "Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin.") c.config = &federationRelationshipConfig{} appendConfigFlags(c.config, f) + cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, c.prettyPrintCreate) } -func (c *createCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *createCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { federationRelationships, err := getRelationships(c.config, c.path) if err != nil { return err } + c.federationRelationships = federationRelationships client := serverClient.NewTrustDomainClient() @@ -61,17 +68,25 @@ func (c *createCommand) Run(ctx context.Context, env *common_cli.Env, serverClie return fmt.Errorf("request failed: %w", err) } + return c.printer.PrintProto(resp) +} + +func (c *createCommand) prettyPrintCreate(env *commoncli.Env, results ...interface{}) error { + createResp, ok := results[0].(*trustdomainv1.BatchCreateFederationRelationshipResponse) + if !ok || len(c.federationRelationships) < len(createResp.Results) { + return cliprinter.ErrInternalCustomPrettyFunc + } // Process results var succeeded []*trustdomainv1.BatchCreateFederationRelationshipResponse_Result var failed []*trustdomainv1.BatchCreateFederationRelationshipResponse_Result - for i, r := range resp.Results { + for i, r := range createResp.Results { switch r.Status.Code { case int32(codes.OK): succeeded = append(succeeded, r) default: // The trust domain API does not include in the results the relationships that // failed to be created, so we populate them from the request data. - r.FederationRelationship = federationRelationships[i] + r.FederationRelationship = c.federationRelationships[i] failed = append(failed, r) } } diff --git a/cmd/spire-server/cli/federation/create_test.go b/cmd/spire-server/cli/federation/create_test.go index f77a70a89b..1211a4d716 100644 --- a/cmd/spire-server/cli/federation/create_test.go +++ b/cmd/spire-server/cli/federation/create_test.go @@ -2,6 +2,7 @@ package federation import ( "crypto/x509" + "encoding/base64" "errors" "fmt" "testing" @@ -10,8 +11,8 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/spiffe/spire/pkg/common/pemutil" + "github.com/spiffe/spire/pkg/server/api" "github.com/spiffe/spire/test/spiretest" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -21,22 +22,7 @@ func TestCreatetHelp(t *testing.T) { test := setupTest(t, newCreateCommand) test.client.Help() - require.Equal(t, `Usage of federation create: - -bundleEndpointProfile string - Endpoint profile type (either "https_web" or "https_spiffe") - -bundleEndpointURL string - URL of the SPIFFE bundle endpoint that provides the trust bundle (must use the HTTPS protocol) - -data string - Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin. - -endpointSpiffeID string - SPIFFE ID of the SPIFFE bundle endpoint server. Only used for 'spiffe' profile.`+common.AddrUsage+ - ` -trustDomain string - Name of the trust domain to federate with (e.g., example.org) - -trustDomainBundleFormat string - The format of the bundle data (optional). Either "pem" or "spiffe". (default "pem") - -trustDomainBundlePath string - Path to the trust domain bundle data (optional). -`, test.stderr.String()) + require.Equal(t, createUsage, test.stderr.String()) } func TestCreateSynopsis(t *testing.T) { @@ -153,48 +139,58 @@ func TestCreate(t *testing.T) { fakeResp *trustdomainv1.BatchCreateFederationRelationshipResponse serverErr error - expOut string - expErr string + expOutPretty string + expOutJSON string + expErrPretty string + expErrJSON string }{ { - name: "Missing trust domain", - expErr: "Error: trust domain is required\n", + name: "Missing trust domain", + expErrPretty: "Error: trust domain is required\n", + expErrJSON: "Error: trust domain is required\n", }, { - name: "Missing bundle endpoint URL", - args: []string{"-trustDomain", "td.org"}, - expErr: "Error: bundle endpoint URL is required\n", + name: "Missing bundle endpoint URL", + args: []string{"-trustDomain", "td.org"}, + expErrPretty: "Error: bundle endpoint URL is required\n", + expErrJSON: "Error: bundle endpoint URL is required\n", }, { - name: "Unknown endpoint profile", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "bad-type"}, - expErr: "Error: unknown bundle endpoint profile type: \"bad-type\"\n", + name: "Unknown endpoint profile", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "bad-type"}, + expErrPretty: "Error: unknown bundle endpoint profile type: \"bad-type\"\n", + expErrJSON: "Error: unknown bundle endpoint profile type: \"bad-type\"\n", }, { - name: "Missing endpoint SPIFFE ID", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", profileHTTPSSPIFFE}, - expErr: "Error: endpoint SPIFFE ID is required if 'https_spiffe' endpoint profile is set\n", + name: "Missing endpoint SPIFFE ID", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", profileHTTPSSPIFFE}, + expErrPretty: "Error: endpoint SPIFFE ID is required if 'https_spiffe' endpoint profile is set\n", + expErrJSON: "Error: endpoint SPIFFE ID is required if 'https_spiffe' endpoint profile is set\n", }, { - name: "Invalid bundle endpoint SPIFFE ID", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "invalid-id", "-trustDomainBundlePath", bundlePath, "-bundleEndpointProfile", profileHTTPSSPIFFE}, - expErr: "Error: cannot parse bundle endpoint SPIFFE ID: scheme is missing or invalid\n", + name: "Invalid bundle endpoint SPIFFE ID", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "invalid-id", "-trustDomainBundlePath", bundlePath, "-bundleEndpointProfile", profileHTTPSSPIFFE}, + expErrPretty: "Error: cannot parse bundle endpoint SPIFFE ID: scheme is missing or invalid\n", + expErrJSON: "Error: cannot parse bundle endpoint SPIFFE ID: scheme is missing or invalid\n", }, { - name: "Non-existent bundle file", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", "non-existent-path", "-bundleEndpointProfile", profileHTTPSWeb}, - expErr: fmt.Sprintf("Error: cannot read bundle file: open non-existent-path: %s\n", spiretest.FileNotFound()), + name: "Non-existent bundle file", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", "non-existent-path", "-bundleEndpointProfile", profileHTTPSWeb}, + expErrPretty: fmt.Sprintf("Error: cannot read bundle file: open non-existent-path: %s\n", spiretest.FileNotFound()), + expErrJSON: fmt.Sprintf("Error: cannot read bundle file: open non-existent-path: %s\n", spiretest.FileNotFound()), }, { - name: "Corrupted bundle file", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", corruptedBundlePath, "-bundleEndpointProfile", profileHTTPSWeb}, - expErr: "Error: cannot parse bundle file: unable to parse bundle data: no PEM blocks\n", + name: "Corrupted bundle file", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", corruptedBundlePath, "-bundleEndpointProfile", profileHTTPSWeb}, + expErrPretty: "Error: cannot parse bundle file: unable to parse bundle data: no PEM blocks\n", + expErrJSON: "Error: cannot parse bundle file: unable to parse bundle data: no PEM blocks\n", }, { - name: "Server error", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "https_web"}, - serverErr: errors.New("server error"), - expErr: "Error: request failed: rpc error: code = Unknown desc = server error\n", + name: "Server error", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "https_web"}, + serverErr: errors.New("server error"), + expErrPretty: "Error: request failed: rpc error: code = Unknown desc = server error\n", + expErrJSON: "Error: request failed: rpc error: code = Unknown desc = server error\n", }, { name: "Succeeds for SPIFFE profile", @@ -205,17 +201,35 @@ func TestCreate(t *testing.T) { fakeResp: &trustdomainv1.BatchCreateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchCreateFederationRelationshipResponse_Result{ { - Status: &types.Status{}, + Status: api.OK(), FederationRelationship: frSPIFFE, }, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-2.org Bundle endpoint URL : https://td-2.org/bundle Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://other.org/bundle `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-2.org", + "bundle_endpoint_url": "https://td-2.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://other.org/bundle" + }, + "trust_domain_bundle": null + } + } + ] +}`, }, { name: "Succeeds for SPIFFE profile and bundle", @@ -226,17 +240,45 @@ Endpoint SPIFFE ID : spiffe://other.org/bundle fakeResp: &trustdomainv1.BatchCreateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchCreateFederationRelationshipResponse_Result{ { - Status: &types.Status{}, + Status: api.OK(), FederationRelationship: frSPIFFEAndBundle, }, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-3.org Bundle endpoint URL : https://td-3.org/bundle Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://td-3.org/bundle `, + expOutJSON: fmt.Sprintf(`{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-3.org", + "bundle_endpoint_url": "https://td-3.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://td-3.org/bundle" + }, + "trust_domain_bundle": { + "trust_domain": "td-3.org", + "x509_authorities": [ + { + "asn1": "%s" + } + ], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } + } + } + ] +}`, base64.StdEncoding.EncodeToString(bundle.X509Authorities[0].Asn1)), }, { name: "Succeeds for web profile", @@ -247,16 +289,32 @@ Endpoint SPIFFE ID : spiffe://td-3.org/bundle fakeResp: &trustdomainv1.BatchCreateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchCreateFederationRelationshipResponse_Result{ { - Status: &types.Status{}, + Status: api.OK(), FederationRelationship: frWeb, }, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-1.org Bundle endpoint URL : https://td-1.org/bundle Bundle endpoint profile : https_web `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-1.org", + "bundle_endpoint_url": "https://td-1.org/bundle", + "https_web": {}, + "trust_domain_bundle": null + } + } + ] +}`, }, { name: "Federation relationships that failed to be created are printed", @@ -275,12 +333,28 @@ Bundle endpoint profile : https_web }, }, }, - expErr: `Failed to create the following federation relationship (code: AlreadyExists, msg: "the message"): + expErrPretty: `Failed to create the following federation relationship (code: AlreadyExists, msg: "the message"): Trust domain : td-1.org Bundle endpoint URL : https://td-1.org/bundle Bundle endpoint profile : https_web Error: failed to create one or more federation relationships `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 6, + "message": "the message" + }, + "federation_relationship": { + "trust_domain": "td-1.org", + "bundle_endpoint_url": "https://td-1.org/bundle", + "https_web": {}, + "trust_domain_bundle": null + } + } + ] +}`, }, { name: "Succeeds loading federation relationships from JSON file", @@ -295,12 +369,12 @@ Error: failed to create one or more federation relationships }, fakeResp: &trustdomainv1.BatchCreateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchCreateFederationRelationshipResponse_Result{ - {FederationRelationship: frWeb, Status: &types.Status{}}, - {FederationRelationship: frSPIFFE, Status: &types.Status{}}, - {FederationRelationship: frPemAuthority, Status: &types.Status{}}, + {FederationRelationship: frWeb, Status: api.OK()}, + {FederationRelationship: frSPIFFE, Status: api.OK()}, + {FederationRelationship: frPemAuthority, Status: api.OK()}, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-1.org Bundle endpoint URL : https://td-1.org/bundle Bundle endpoint profile : https_web @@ -315,44 +389,110 @@ Bundle endpoint URL : https://td-3.org/bundle Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://td-3.org/bundle `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-1.org", + "bundle_endpoint_url": "https://td-1.org/bundle", + "https_web": {}, + "trust_domain_bundle": null + } + }, + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-2.org", + "bundle_endpoint_url": "https://td-2.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://other.org/bundle" + }, + "trust_domain_bundle": null + } + }, + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-3.org", + "bundle_endpoint_url": "https://td-3.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://td-3.org/bundle" + }, + "trust_domain_bundle": { + "trust_domain": "td-3.org", + "x509_authorities": [ + { + "asn1": "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyvsCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09Xmakw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylAdZglS5kKnYigmwDh+/U=" + } + ], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } + } + } + ] +}`, }, { - name: "Loading federation relationships from JSON file: invalid path", - args: []string{"-data", "somePath"}, - expErr: fmt.Sprintf("Error: open somePath: %s\n", spiretest.FileNotFound()), + name: "Loading federation relationships from JSON file: invalid path", + args: []string{"-data", "somePath"}, + expErrPretty: fmt.Sprintf("Error: open somePath: %s\n", spiretest.FileNotFound()), + expErrJSON: fmt.Sprintf("Error: open somePath: %s\n", spiretest.FileNotFound()), }, { - name: "Loading federation relationships from JSON file: no a json", - args: []string{"-data", bundlePath}, - expErr: "Error: failed to parse JSON: invalid character '-' in numeric literal\n", + name: "Loading federation relationships from JSON file: no a json", + args: []string{"-data", bundlePath}, + expErrPretty: "Error: failed to parse JSON: invalid character '-' in numeric literal\n", + expErrJSON: "Error: failed to parse JSON: invalid character '-' in numeric literal\n", }, { - name: "Loading federation relationships from JSON file: invalid relationship", - args: []string{"-data", jsonDataInvalidRelationship}, - expErr: "Error: could not parse item 0: trust domain is required\n", + name: "Loading federation relationships from JSON file: invalid relationship", + args: []string{"-data", jsonDataInvalidRelationship}, + expErrPretty: "Error: could not parse item 0: trust domain is required\n", + expErrJSON: "Error: could not parse item 0: trust domain is required\n", }, { - name: "Loading federation relationships from JSON file: multiple flags", - args: []string{"-data", jsonDataInvalidRelationship, "-bundleEndpointURL", "https://td-1.org/bundle"}, - expErr: "Error: cannot use other flags to specify relationship fields when 'data' flag is set\n", + name: "Loading federation relationships from JSON file: multiple flags", + args: []string{"-data", jsonDataInvalidRelationship, "-bundleEndpointURL", "https://td-1.org/bundle"}, + expErrPretty: "Error: cannot use other flags to specify relationship fields when 'data' flag is set\n", + expErrJSON: "Error: cannot use other flags to specify relationship fields when 'data' flag is set\n", }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newCreateCommand) - test.server.err = tt.serverErr - test.server.expectCreateReq = tt.expReq - test.server.createResp = tt.fakeResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newCreateCommand) + test.server.err = tt.serverErr + test.server.expectCreateReq = tt.expReq + test.server.createResp = tt.fakeResp + args := tt.args + args = append(args, "-output", format) - rc := test.client.Run(test.args(tt.args...)) - if tt.expErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expErr, test.stderr.String()) - return - } + rc := test.client.Run(test.args(args...)) + if tt.expErrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrPretty, test.stderr.String()) + return + } + if tt.expErrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrJSON, test.stderr.String()) + return + } - require.Equal(t, 0, rc) - require.Equal(t, tt.expOut, test.stdout.String()) - }) + require.Equal(t, 0, rc) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expOutPretty, tt.expOutJSON) + }) + } } } diff --git a/cmd/spire-server/cli/federation/delete.go b/cmd/spire-server/cli/federation/delete.go index 98a798af2e..c67d68e34f 100644 --- a/cmd/spire-server/cli/federation/delete.go +++ b/cmd/spire-server/cli/federation/delete.go @@ -9,21 +9,24 @@ import ( "github.com/mitchellh/cli" "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "google.golang.org/grpc/codes" ) func NewDeleteCommand() cli.Command { - return newDeleteCommand(common_cli.DefaultEnv) + return newDeleteCommand(commoncli.DefaultEnv) } -func newDeleteCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(deleteCommand)) +func newDeleteCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &deleteCommand{env: env}) } type deleteCommand struct { // SPIFFE ID of the trust domain to delete - id string + id string + env *commoncli.Env + printer cliprinter.Printer } func (c *deleteCommand) Name() string { @@ -36,9 +39,10 @@ func (c *deleteCommand) Synopsis() string { func (c *deleteCommand) AppendFlags(fs *flag.FlagSet) { fs.StringVar(&c.id, "id", "", "SPIFFE ID of the trust domain") + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintDelete) } -func (c *deleteCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *deleteCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if c.id == "" { return errors.New("id is required") } @@ -50,13 +54,20 @@ func (c *deleteCommand) Run(ctx context.Context, env *common_cli.Env, serverClie if err != nil { return fmt.Errorf("failed to delete federation relationship: %w", err) } + return c.printer.PrintProto(resp) +} - result := resp.Results[0] - switch result.Status.Code { - case int32(codes.OK): - env.Println("federation relationship deleted.") - return nil - default: - return fmt.Errorf("failed to delete federation relationship %q: %s", result.TrustDomain, result.Status.Message) +func prettyPrintDelete(env *commoncli.Env, results ...interface{}) error { + if deleteResp, ok := results[0].(*trustdomain.BatchDeleteFederationRelationshipResponse); ok && len(deleteResp.Results) > 0 { + result := deleteResp.Results[0] + switch result.Status.Code { + case int32(codes.OK): + env.Println("federation relationship deleted.") + return nil + default: + return fmt.Errorf("failed to delete federation relationship %q: %s", result.TrustDomain, result.Status.Message) + } } + + return cliprinter.ErrInternalCustomPrettyFunc } diff --git a/cmd/spire-server/cli/federation/delete_test.go b/cmd/spire-server/cli/federation/delete_test.go index b2755ace07..2c7f498a87 100644 --- a/cmd/spire-server/cli/federation/delete_test.go +++ b/cmd/spire-server/cli/federation/delete_test.go @@ -1,11 +1,12 @@ package federation import ( + "fmt" "testing" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" - "github.com/spiffe/spire/cmd/spire-server/cli/common" + "github.com/spiffe/spire/pkg/server/api" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -15,9 +16,7 @@ func TestDeleteHelp(t *testing.T) { test := setupTest(t, newDeleteCommand) test.client.Help() - require.Equal(t, `Usage of federation delete: - -id string - SPIFFE ID of the trust domain`+common.AddrUsage, test.stderr.String()) + require.Equal(t, deleteUsage, test.stderr.String()) } func TestDeleteSynopsis(t *testing.T) { @@ -34,8 +33,10 @@ func TestDelete(t *testing.T) { deleteResp *trustdomainv1.BatchDeleteFederationRelationshipResponse serverErr error - expectOut string - expectErr string + expectOutPretty string + expectOutJSON string + expectErrPretty string + expectErrJSON string }{ { name: "Success", @@ -46,22 +47,26 @@ func TestDelete(t *testing.T) { deleteResp: &trustdomainv1.BatchDeleteFederationRelationshipResponse{ Results: []*trustdomainv1.BatchDeleteFederationRelationshipResponse_Result{ { - Status: &types.Status{Code: int32(codes.OK)}, + Status: api.OK(), TrustDomain: "example.org", }, }, }, - expectOut: "federation relationship deleted.\n", + expectOutPretty: "federation relationship deleted.\n", + expectOutJSON: `{"results":[{"status":{"code":0,"message":"OK"},"trust_domain":"example.org"}]}`, }, { - name: "Empty ID", - expectErr: "Error: id is required\n", + name: "Empty ID", + expectErrPretty: "Error: id is required\n", + expectErrJSON: "Error: id is required\n", }, { name: "Server client fails", args: []string{"-id", "spiffe://example.org"}, serverErr: status.Error(codes.Internal, "oh! no"), - expectErr: `Error: failed to delete federation relationship: rpc error: code = Internal desc = oh! no + expectErrPretty: `Error: failed to delete federation relationship: rpc error: code = Internal desc = oh! no +`, + expectErrJSON: `Error: failed to delete federation relationship: rpc error: code = Internal desc = oh! no `, }, { @@ -81,26 +86,37 @@ func TestDelete(t *testing.T) { }, }, }, - expectErr: `Error: failed to delete federation relationship "example.org": oh! no + expectErrPretty: `Error: failed to delete federation relationship "example.org": oh! no `, + expectOutJSON: `{"results":[{"status":{"code":13,"message":"oh! no"},"trust_domain":"example.org"}]}`, }, } { - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newDeleteCommand) - test.server.err = tt.serverErr - test.server.expectDeleteReq = tt.expectReq - test.server.deleteResp = tt.deleteResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newDeleteCommand) + test.server.err = tt.serverErr + test.server.expectDeleteReq = tt.expectReq + test.server.deleteResp = tt.deleteResp + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(args...)) - rc := test.client.Run(test.args(tt.args...)) - if tt.expectErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expectErr, test.stderr.String()) - return - } + if tt.expectErrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectErrPretty, test.stderr.String()) + return + } + if tt.expectErrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectErrJSON, test.stderr.String()) + return + } - require.Equal(t, 0, rc) - require.Equal(t, tt.expectOut, test.stdout.String()) - require.Empty(t, test.stderr.String()) - }) + require.Equal(t, 0, rc) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectOutPretty, tt.expectOutJSON) + require.Empty(t, test.stderr.String()) + }) + } } } diff --git a/cmd/spire-server/cli/federation/list.go b/cmd/spire-server/cli/federation/list.go index c7893635ce..7fb536bc9c 100644 --- a/cmd/spire-server/cli/federation/list.go +++ b/cmd/spire-server/cli/federation/list.go @@ -8,18 +8,21 @@ import ( "github.com/mitchellh/cli" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" ) func NewListCommand() cli.Command { - return newListCommand(common_cli.DefaultEnv) + return newListCommand(commoncli.DefaultEnv) } -func newListCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(listCommand)) +func newListCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &listCommand{env: env}) } type listCommand struct { + env *commoncli.Env + printer cliprinter.Printer } func (c *listCommand) Name() string { @@ -31,21 +34,29 @@ func (c *listCommand) Synopsis() string { } func (c *listCommand) AppendFlags(fs *flag.FlagSet) { + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintList) } -func (c *listCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *listCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { trustDomainClient := serverClient.NewTrustDomainClient() resp, err := trustDomainClient.ListFederationRelationships(ctx, &trustdomainv1.ListFederationRelationshipsRequest{}) if err != nil { return fmt.Errorf("error listing federation relationship: %w", err) } + return c.printer.PrintProto(resp) +} - msg := fmt.Sprintf("Found %v ", len(resp.FederationRelationships)) - msg = util.Pluralizer(msg, "federation relationship", "federation relationships", len(resp.FederationRelationships)) +func prettyPrintList(env *commoncli.Env, results ...interface{}) error { + listResp, ok := results[0].(*trustdomainv1.ListFederationRelationshipsResponse) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } + msg := fmt.Sprintf("Found %v ", len(listResp.FederationRelationships)) + msg = util.Pluralizer(msg, "federation relationship", "federation relationships", len(listResp.FederationRelationships)) env.Println(msg) - for _, fr := range resp.FederationRelationships { + for _, fr := range listResp.FederationRelationships { env.Println() printFederationRelationship(fr, env.Printf) } diff --git a/cmd/spire-server/cli/federation/list_test.go b/cmd/spire-server/cli/federation/list_test.go index e4105db073..9b784a6126 100644 --- a/cmd/spire-server/cli/federation/list_test.go +++ b/cmd/spire-server/cli/federation/list_test.go @@ -1,11 +1,11 @@ package federation import ( + "fmt" "testing" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -15,7 +15,7 @@ func TestListHelp(t *testing.T) { test := setupTest(t, newListCommand) test.client.Help() - require.Equal(t, `Usage of federation list:`+common.AddrUsage, test.stderr.String()) + require.Equal(t, listUsage, test.stderr.String()) } func TestListSynopsis(t *testing.T) { @@ -59,14 +59,19 @@ func TestList(t *testing.T) { serverErr error - expectOut string - expectErr string + expectOutPretty string + expectOutJSON string + expectErr string }{ { - name: "no federations", - expectListReq: &trustdomainv1.ListFederationRelationshipsRequest{}, - listResp: &trustdomainv1.ListFederationRelationshipsResponse{}, - expectOut: "Found 0 federation relationships\n", + name: "no federations", + expectListReq: &trustdomainv1.ListFederationRelationshipsRequest{}, + listResp: &trustdomainv1.ListFederationRelationshipsResponse{}, + expectOutPretty: "Found 0 federation relationships\n", + expectOutJSON: `{ + "federation_relationships": [], + "next_page_token": "" +}`, }, { name: "single federation", @@ -74,12 +79,23 @@ func TestList(t *testing.T) { listResp: &trustdomainv1.ListFederationRelationshipsResponse{ FederationRelationships: []*types.FederationRelationship{federation1}, }, - expectOut: `Found 1 federation relationship + expectOutPretty: `Found 1 federation relationship Trust domain : foh.test Bundle endpoint URL : https://foo.test/endpoint Bundle endpoint profile : https_web `, + expectOutJSON: `{ + "federation_relationships": [ + { + "trust_domain": "foh.test", + "bundle_endpoint_url": "https://foo.test/endpoint", + "https_web": {}, + "trust_domain_bundle": null + } + ], + "next_page_token": "" +}`, }, { name: "multiple federations", @@ -91,7 +107,7 @@ Bundle endpoint profile : https_web federation3, }, }, - expectOut: `Found 3 federation relationships + expectOutPretty: `Found 3 federation relationships Trust domain : foh.test Bundle endpoint URL : https://foo.test/endpoint @@ -107,6 +123,39 @@ Bundle endpoint URL : https://baz.test/endpoint Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://baz.test/id `, + expectOutJSON: `{ + "federation_relationships": [ + { + "trust_domain": "foh.test", + "bundle_endpoint_url": "https://foo.test/endpoint", + "https_web": {}, + "trust_domain_bundle": null + }, + { + "trust_domain": "bar.test", + "bundle_endpoint_url": "https://bar.test/endpoint", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://bar.test/id" + }, + "trust_domain_bundle": { + "trust_domain": "bar.test", + "x509_authorities": [], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } + }, + { + "trust_domain": "baz.test", + "bundle_endpoint_url": "https://baz.test/endpoint", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://baz.test/id" + }, + "trust_domain_bundle": null + } + ], + "next_page_token": "" +}`, }, { name: "server fails", @@ -114,21 +163,25 @@ Endpoint SPIFFE ID : spiffe://baz.test/id expectErr: "Error: error listing federation relationship: rpc error: code = Internal desc = oh! no\n", }, } { - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newListCommand) - test.server.err = tt.serverErr - test.server.expectListReq = tt.expectListReq - test.server.listResp = tt.listResp - - rc := test.client.Run(test.args(tt.args...)) - if tt.expectErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expectErr, test.stderr.String()) - return - } - - require.Equal(t, 0, rc) - require.Equal(t, tt.expectOut, test.stdout.String()) - }) + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newListCommand) + test.server.err = tt.serverErr + test.server.expectListReq = tt.expectListReq + test.server.listResp = tt.listResp + args := tt.args + args = append(args, "-output", format) + + rc := test.client.Run(test.args(args...)) + if tt.expectErr != "" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectErr, test.stderr.String()) + return + } + + require.Equal(t, 0, rc) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectOutPretty, tt.expectOutJSON) + }) + } } } diff --git a/cmd/spire-server/cli/federation/refresh.go b/cmd/spire-server/cli/federation/refresh.go index 1fc5a1e344..714fc38ba8 100644 --- a/cmd/spire-server/cli/federation/refresh.go +++ b/cmd/spire-server/cli/federation/refresh.go @@ -9,21 +9,25 @@ import ( "github.com/mitchellh/cli" "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" + "github.com/spiffe/spire/pkg/server/api" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func NewRefreshCommand() cli.Command { - return newRefreshCommand(common_cli.DefaultEnv) + return newRefreshCommand(commoncli.DefaultEnv) } -func newRefreshCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(refreshCommand)) +func newRefreshCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &refreshCommand{env: env}) } type refreshCommand struct { - id string + id string + env *commoncli.Env + printer cliprinter.Printer } func (c *refreshCommand) Name() string { @@ -36,9 +40,10 @@ func (c *refreshCommand) Synopsis() string { func (c *refreshCommand) AppendFlags(fs *flag.FlagSet) { fs.StringVar(&c.id, "id", "", "SPIFFE ID of the trust domain") + cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintRefresh) } -func (c *refreshCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *refreshCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if c.id == "" { return errors.New("id is required") } @@ -47,13 +52,17 @@ func (c *refreshCommand) Run(ctx context.Context, env *common_cli.Env, serverCli _, err := trustDomainClient.RefreshBundle(ctx, &trustdomain.RefreshBundleRequest{ TrustDomain: c.id, }) + switch status.Code(err) { case codes.OK: - env.Println("Bundle refreshed") - return nil + return c.printer.PrintProto(api.OK()) case codes.NotFound: return fmt.Errorf("there is no federation relationship with trust domain %q", c.id) default: return fmt.Errorf("failed to refresh bundle: %w", err) } } + +func prettyPrintRefresh(env *commoncli.Env, _ ...interface{}) error { + return env.Println("Bundle refreshed") +} diff --git a/cmd/spire-server/cli/federation/refresh_test.go b/cmd/spire-server/cli/federation/refresh_test.go index 058bae573b..87635278d1 100644 --- a/cmd/spire-server/cli/federation/refresh_test.go +++ b/cmd/spire-server/cli/federation/refresh_test.go @@ -1,10 +1,10 @@ package federation import ( + "fmt" "testing" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -15,9 +15,7 @@ func TestRefreshHelp(t *testing.T) { test := setupTest(t, newRefreshCommand) test.client.Help() - require.Equal(t, `Usage of federation refresh: - -id string - SPIFFE ID of the trust domain`+common.AddrUsage, test.stderr.String()) + require.Equal(t, refreshUsage, test.stderr.String()) } func TestRefreshSynopsis(t *testing.T) { @@ -34,8 +32,9 @@ func TestRefresh(t *testing.T) { refreshResp *emptypb.Empty serverErr error - expectOut string - expectErr string + expectOutPretty string + expectOutJSON string + expectErr string }{ { name: "Success", @@ -43,8 +42,9 @@ func TestRefresh(t *testing.T) { expectReq: &trustdomainv1.RefreshBundleRequest{ TrustDomain: "spiffe://example.org", }, - expectOut: "Bundle refreshed\n", - refreshResp: &emptypb.Empty{}, + expectOutPretty: "Bundle refreshed\n", + expectOutJSON: `{"code":0,"message":"OK"}`, + refreshResp: &emptypb.Empty{}, }, { name: "Empty ID", @@ -65,22 +65,26 @@ func TestRefresh(t *testing.T) { `, }, } { - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newRefreshCommand) - test.server.err = tt.serverErr - test.server.expectRefreshReq = tt.expectReq - test.server.refreshResp = tt.refreshResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newRefreshCommand) + test.server.err = tt.serverErr + test.server.expectRefreshReq = tt.expectReq + test.server.refreshResp = tt.refreshResp + args := tt.args + args = append(args, "-output", format) - rc := test.client.Run(test.args(tt.args...)) - if tt.expectErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expectErr, test.stderr.String()) - return - } + rc := test.client.Run(test.args(args...)) + if tt.expectErr != "" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectErr, test.stderr.String()) + return + } - require.Equal(t, 0, rc) - require.Equal(t, tt.expectOut, test.stdout.String()) - require.Empty(t, test.stderr.String()) - }) + require.Equal(t, 0, rc) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectOutPretty, tt.expectOutJSON) + require.Empty(t, test.stderr.String()) + }) + } } } diff --git a/cmd/spire-server/cli/federation/show.go b/cmd/spire-server/cli/federation/show.go index c5f4502252..40f0af8f3c 100644 --- a/cmd/spire-server/cli/federation/show.go +++ b/cmd/spire-server/cli/federation/show.go @@ -8,21 +8,25 @@ import ( "github.com/mitchellh/cli" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" + prototypes "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" ) func NewShowCommand() cli.Command { - return newShowCommand(common_cli.DefaultEnv) + return newShowCommand(commoncli.DefaultEnv) } -func newShowCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(showCommand)) +func newShowCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &showCommand{env: env}) } type showCommand struct { // Trust domain name of the federation relationship to show trustDomain string + env *commoncli.Env + printer cliprinter.Printer } func (c *showCommand) Name() string { @@ -35,9 +39,10 @@ func (c *showCommand) Synopsis() string { func (c *showCommand) AppendFlags(f *flag.FlagSet) { f.StringVar(&c.trustDomain, "trustDomain", "", "The trust domain name of the federation relationship to show") + cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, c.prettyPrintShow) } -func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *showCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { if c.trustDomain == "" { return errors.New("a trust domain name is required") } @@ -51,6 +56,14 @@ func (c *showCommand) Run(ctx context.Context, env *common_cli.Env, serverClient return fmt.Errorf("error showing federation relationship: %w", err) } + return c.printer.PrintProto(fr) +} + +func (c *showCommand) prettyPrintShow(env *commoncli.Env, results ...interface{}) error { + fr, ok := results[0].(*prototypes.FederationRelationship) + if !ok { + return cliprinter.ErrInternalCustomPrettyFunc + } env.Printf("Found a federation relationship with trust domain %s:\n\n", c.trustDomain) printFederationRelationship(fr, env.Printf) diff --git a/cmd/spire-server/cli/federation/show_test.go b/cmd/spire-server/cli/federation/show_test.go index eaf0467aa2..22d1ff0176 100644 --- a/cmd/spire-server/cli/federation/show_test.go +++ b/cmd/spire-server/cli/federation/show_test.go @@ -1,11 +1,11 @@ package federation import ( + "fmt" "testing" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -15,10 +15,7 @@ func TestShowHelp(t *testing.T) { test := setupTest(t, newShowCommand) test.client.Help() - require.Equal(t, `Usage of federation show:`+common.AddrUsage+ - ` -trustDomain string - The trust domain name of the federation relationship to show -`, test.stderr.String()) + require.Equal(t, showUsage, test.stderr.String()) } func TestShowSynopsis(t *testing.T) { @@ -52,33 +49,54 @@ func TestShow(t *testing.T) { resp *types.FederationRelationship serverErr error - expectedStdout string - expectedStderr string + expectedStdoutPretty string + expectedStdoutJSON string + expectedStderr string }{ { name: "succeeds https_web", req: &trustdomainv1.GetFederationRelationshipRequest{}, resp: fr1, args: []string{"-trustDomain", "example-1.test"}, - expectedStdout: `Found a federation relationship with trust domain example-1.test: + expectedStdoutPretty: `Found a federation relationship with trust domain example-1.test: Trust domain : example-1.test Bundle endpoint URL : https://bundle-endpoint-1.test/endpoint Bundle endpoint profile : https_web `, + expectedStdoutJSON: `{ + "trust_domain": "example-1.test", + "bundle_endpoint_url": "https://bundle-endpoint-1.test/endpoint", + "https_web": {}, + "trust_domain_bundle": null +}`, }, { name: "succeeds https_spiffe", req: &trustdomainv1.GetFederationRelationshipRequest{}, resp: fr2, args: []string{"-trustDomain", "example-2.test"}, - expectedStdout: `Found a federation relationship with trust domain example-2.test: + expectedStdoutPretty: `Found a federation relationship with trust domain example-2.test: Trust domain : example-2.test Bundle endpoint URL : https://bundle-endpoint-2.test/endpoint Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://endpoint.test/id `, + expectedStdoutJSON: `{ + "trust_domain": "example-2.test", + "bundle_endpoint_url": "https://bundle-endpoint-2.test/endpoint", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://endpoint.test/id" + }, + "trust_domain_bundle": { + "trust_domain": "endpoint.test", + "x509_authorities": [], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } +}`, }, { name: "server fails", @@ -95,21 +113,24 @@ Endpoint SPIFFE ID : spiffe://endpoint.test/id expectedStderr: "Error: a trust domain name is required\n", }, } { - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newShowCommand) - test.server.err = tt.serverErr - test.server.expectShowReq = tt.req - test.server.showResp = tt.resp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newShowCommand) + test.server.err = tt.serverErr + test.server.expectShowReq = tt.req + test.server.showResp = tt.resp + args := tt.args + args = append(args, "-output", format) - rc := test.client.Run(test.args(tt.args...)) - if tt.expectedStderr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expectedStderr, test.stderr.String()) - return - } - - require.Equal(t, 0, rc) - require.Equal(t, tt.expectedStdout, test.stdout.String()) - }) + rc := test.client.Run(test.args(args...)) + if tt.expectedStderr != "" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expectedStderr, test.stderr.String()) + return + } + require.Equal(t, 0, rc) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expectedStdoutPretty, tt.expectedStdoutJSON) + }) + } } } diff --git a/cmd/spire-server/cli/federation/update.go b/cmd/spire-server/cli/federation/update.go index 69c4fa56eb..f86715356c 100644 --- a/cmd/spire-server/cli/federation/update.go +++ b/cmd/spire-server/cli/federation/update.go @@ -8,23 +8,28 @@ import ( "github.com/mitchellh/cli" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" + "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" - common_cli "github.com/spiffe/spire/pkg/common/cli" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/cliprinter" "google.golang.org/grpc/codes" ) // NewUpdateCommand creates a new "update" subcommand for "federation" command. func NewUpdateCommand() cli.Command { - return newUpdateCommand(common_cli.DefaultEnv) + return newUpdateCommand(commoncli.DefaultEnv) } -func newUpdateCommand(env *common_cli.Env) cli.Command { - return util.AdaptCommand(env, new(updateCommand)) +func newUpdateCommand(env *commoncli.Env) cli.Command { + return util.AdaptCommand(env, &updateCommand{env: env}) } type updateCommand struct { - path string - config *federationRelationshipConfig + path string + config *federationRelationshipConfig + env *commoncli.Env + printer cliprinter.Printer + federationRelationships []*types.FederationRelationship } func (*updateCommand) Name() string { @@ -39,34 +44,45 @@ func (c *updateCommand) AppendFlags(f *flag.FlagSet) { f.StringVar(&c.path, "data", "", "Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin.") c.config = &federationRelationshipConfig{} appendConfigFlags(c.config, f) + cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, c.prettyPrintUpdate) } -func (c *updateCommand) Run(ctx context.Context, env *common_cli.Env, serverClient util.ServerClient) error { +func (c *updateCommand) Run(ctx context.Context, env *commoncli.Env, serverClient util.ServerClient) error { federationRelationships, err := getRelationships(c.config, c.path) if err != nil { return err } + c.federationRelationships = federationRelationships client := serverClient.NewTrustDomainClient() resp, err := client.BatchUpdateFederationRelationship(ctx, &trustdomainv1.BatchUpdateFederationRelationshipRequest{ - FederationRelationships: federationRelationships, + FederationRelationships: c.federationRelationships, }) if err != nil { return fmt.Errorf("request failed: %w", err) } + return c.printer.PrintProto(resp) +} + +func (c *updateCommand) prettyPrintUpdate(env *commoncli.Env, results ...interface{}) error { + updateResp, ok := results[0].(*trustdomainv1.BatchUpdateFederationRelationshipResponse) + if !ok || len(c.federationRelationships) < len(updateResp.Results) { + return cliprinter.ErrInternalCustomPrettyFunc + } + // Process results var succeeded []*trustdomainv1.BatchUpdateFederationRelationshipResponse_Result var failed []*trustdomainv1.BatchUpdateFederationRelationshipResponse_Result - for i, r := range resp.Results { + for i, r := range updateResp.Results { switch r.Status.Code { case int32(codes.OK): succeeded = append(succeeded, r) default: // The trust domain API does not include in the results the relationships that // failed to be updated, so we populate them from the request data. - r.FederationRelationship = federationRelationships[i] + r.FederationRelationship = c.federationRelationships[i] failed = append(failed, r) } } diff --git a/cmd/spire-server/cli/federation/update_test.go b/cmd/spire-server/cli/federation/update_test.go index d22ab63b8e..58733e5a4e 100644 --- a/cmd/spire-server/cli/federation/update_test.go +++ b/cmd/spire-server/cli/federation/update_test.go @@ -2,6 +2,7 @@ package federation import ( "crypto/x509" + "encoding/base64" "errors" "fmt" "testing" @@ -10,8 +11,8 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" "github.com/spiffe/spire-api-sdk/proto/spire/api/types" - "github.com/spiffe/spire/cmd/spire-server/cli/common" "github.com/spiffe/spire/pkg/common/pemutil" + "github.com/spiffe/spire/pkg/server/api" "github.com/spiffe/spire/test/spiretest" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -21,22 +22,7 @@ func TestUpdateHelp(t *testing.T) { test := setupTest(t, newUpdateCommand) test.client.Help() - require.Equal(t, `Usage of federation update: - -bundleEndpointProfile string - Endpoint profile type (either "https_web" or "https_spiffe") - -bundleEndpointURL string - URL of the SPIFFE bundle endpoint that provides the trust bundle (must use the HTTPS protocol) - -data string - Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin. - -endpointSpiffeID string - SPIFFE ID of the SPIFFE bundle endpoint server. Only used for 'spiffe' profile.`+common.AddrUsage+ - ` -trustDomain string - Name of the trust domain to federate with (e.g., example.org) - -trustDomainBundleFormat string - The format of the bundle data (optional). Either "pem" or "spiffe". (default "pem") - -trustDomainBundlePath string - Path to the trust domain bundle data (optional). -`, test.stderr.String()) + require.Equal(t, updateUsage, test.stderr.String()) } func TestUpdateSynopsis(t *testing.T) { @@ -153,48 +139,58 @@ func TestUpdate(t *testing.T) { fakeResp *trustdomainv1.BatchUpdateFederationRelationshipResponse serverErr error - expOut string - expErr string + expOutPretty string + expErrPretty string + expOutJSON string + expErrJSON string }{ { - name: "Missing trust domain", - expErr: "Error: trust domain is required\n", + name: "Missing trust domain", + expErrPretty: "Error: trust domain is required\n", + expErrJSON: "Error: trust domain is required\n", }, { - name: "Missing bundle endpoint URL", - args: []string{"-trustDomain", "td.org"}, - expErr: "Error: bundle endpoint URL is required\n", + name: "Missing bundle endpoint URL", + args: []string{"-trustDomain", "td.org"}, + expErrPretty: "Error: bundle endpoint URL is required\n", + expErrJSON: "Error: bundle endpoint URL is required\n", }, { - name: "Unknown endpoint profile", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "bad-type"}, - expErr: "Error: unknown bundle endpoint profile type: \"bad-type\"\n", + name: "Unknown endpoint profile", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "bad-type"}, + expErrPretty: "Error: unknown bundle endpoint profile type: \"bad-type\"\n", + expErrJSON: "Error: unknown bundle endpoint profile type: \"bad-type\"\n", }, { - name: "Missing endpoint SPIFFE ID", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", profileHTTPSSPIFFE}, - expErr: "Error: endpoint SPIFFE ID is required if 'https_spiffe' endpoint profile is set\n", + name: "Missing endpoint SPIFFE ID", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", profileHTTPSSPIFFE}, + expErrPretty: "Error: endpoint SPIFFE ID is required if 'https_spiffe' endpoint profile is set\n", + expErrJSON: "Error: endpoint SPIFFE ID is required if 'https_spiffe' endpoint profile is set\n", }, { - name: "Invalid bundle endpoint SPIFFE ID", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "invalid-id", "-trustDomainBundlePath", bundlePath, "-bundleEndpointProfile", profileHTTPSSPIFFE}, - expErr: "Error: cannot parse bundle endpoint SPIFFE ID: scheme is missing or invalid\n", + name: "Invalid bundle endpoint SPIFFE ID", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "invalid-id", "-trustDomainBundlePath", bundlePath, "-bundleEndpointProfile", profileHTTPSSPIFFE}, + expErrPretty: "Error: cannot parse bundle endpoint SPIFFE ID: scheme is missing or invalid\n", + expErrJSON: "Error: cannot parse bundle endpoint SPIFFE ID: scheme is missing or invalid\n", }, { - name: "Non-existent bundle file", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", "non-existent-path", "-bundleEndpointProfile", profileHTTPSWeb}, - expErr: fmt.Sprintf("Error: cannot read bundle file: open non-existent-path: %s\n", spiretest.FileNotFound()), + name: "Non-existent bundle file", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", "non-existent-path", "-bundleEndpointProfile", profileHTTPSWeb}, + expErrPretty: fmt.Sprintf("Error: cannot read bundle file: open non-existent-path: %s\n", spiretest.FileNotFound()), + expErrJSON: fmt.Sprintf("Error: cannot read bundle file: open non-existent-path: %s\n", spiretest.FileNotFound()), }, { - name: "Corrupted bundle file", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", corruptedBundlePath, "-bundleEndpointProfile", profileHTTPSWeb}, - expErr: "Error: cannot parse bundle file: unable to parse bundle data: no PEM blocks\n", + name: "Corrupted bundle file", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-endpointSpiffeID", "spiffe://td.org/bundle", "-trustDomainBundlePath", corruptedBundlePath, "-bundleEndpointProfile", profileHTTPSWeb}, + expErrPretty: "Error: cannot parse bundle file: unable to parse bundle data: no PEM blocks\n", + expErrJSON: "Error: cannot parse bundle file: unable to parse bundle data: no PEM blocks\n", }, { - name: "Server error", - args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "https_web"}, - serverErr: errors.New("server error"), - expErr: "Error: request failed: rpc error: code = Unknown desc = server error\n", + name: "Server error", + args: []string{"-trustDomain", "td.org", "-bundleEndpointURL", "https://td.org/bundle", "-bundleEndpointProfile", "https_web"}, + serverErr: errors.New("server error"), + expErrPretty: "Error: request failed: rpc error: code = Unknown desc = server error\n", + expErrJSON: "Error: request failed: rpc error: code = Unknown desc = server error\n", }, { name: "Succeeds for SPIFFE profile", @@ -205,17 +201,35 @@ func TestUpdate(t *testing.T) { fakeResp: &trustdomainv1.BatchUpdateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchUpdateFederationRelationshipResponse_Result{ { - Status: &types.Status{}, + Status: api.OK(), FederationRelationship: frSPIFFE, }, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-2.org Bundle endpoint URL : https://td-2.org/bundle Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://other.org/bundle `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-2.org", + "bundle_endpoint_url": "https://td-2.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://other.org/bundle" + }, + "trust_domain_bundle": null + } + } + ] +}`, }, { name: "Succeeds for SPIFFE profile and bundle", @@ -226,17 +240,45 @@ Endpoint SPIFFE ID : spiffe://other.org/bundle fakeResp: &trustdomainv1.BatchUpdateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchUpdateFederationRelationshipResponse_Result{ { - Status: &types.Status{}, + Status: api.OK(), FederationRelationship: frSPIFFEAndBundle, }, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-3.org Bundle endpoint URL : https://td-3.org/bundle Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://td-3.org/bundle `, + expOutJSON: fmt.Sprintf(`{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-3.org", + "bundle_endpoint_url": "https://td-3.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://td-3.org/bundle" + }, + "trust_domain_bundle": { + "trust_domain": "td-3.org", + "x509_authorities": [ + { + "asn1": "%s" + } + ], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } + } + } + ] +}`, base64.StdEncoding.EncodeToString(bundle.X509Authorities[0].Asn1)), }, { name: "Succeeds for web profile", @@ -247,16 +289,32 @@ Endpoint SPIFFE ID : spiffe://td-3.org/bundle fakeResp: &trustdomainv1.BatchUpdateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchUpdateFederationRelationshipResponse_Result{ { - Status: &types.Status{}, + Status: api.OK(), FederationRelationship: frWeb, }, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-1.org Bundle endpoint URL : https://td-1.org/bundle Bundle endpoint profile : https_web `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-1.org", + "bundle_endpoint_url": "https://td-1.org/bundle", + "https_web": {}, + "trust_domain_bundle": null + } + } + ] +}`, }, { name: "Federation relationships that failed to be updated are printed", @@ -275,12 +333,28 @@ Bundle endpoint profile : https_web }, }, }, - expErr: `Failed to update the following federation relationship (code: AlreadyExists, msg: "the message"): + expErrPretty: `Failed to update the following federation relationship (code: AlreadyExists, msg: "the message"): Trust domain : td-1.org Bundle endpoint URL : https://td-1.org/bundle Bundle endpoint profile : https_web Error: failed to update one or more federation relationships `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 6, + "message": "the message" + }, + "federation_relationship": { + "trust_domain": "td-1.org", + "bundle_endpoint_url": "https://td-1.org/bundle", + "https_web": {}, + "trust_domain_bundle": null + } + } + ] +}`, }, { name: "Succeeds loading federation relationships from JSON file", @@ -295,12 +369,12 @@ Error: failed to update one or more federation relationships }, fakeResp: &trustdomainv1.BatchUpdateFederationRelationshipResponse{ Results: []*trustdomainv1.BatchUpdateFederationRelationshipResponse_Result{ - {FederationRelationship: frWeb, Status: &types.Status{}}, - {FederationRelationship: frSPIFFE, Status: &types.Status{}}, - {FederationRelationship: frPemAuthority, Status: &types.Status{}}, + {FederationRelationship: frWeb, Status: api.OK()}, + {FederationRelationship: frSPIFFE, Status: api.OK()}, + {FederationRelationship: frPemAuthority, Status: api.OK()}, }, }, - expOut: ` + expOutPretty: ` Trust domain : td-1.org Bundle endpoint URL : https://td-1.org/bundle Bundle endpoint profile : https_web @@ -315,44 +389,110 @@ Bundle endpoint URL : https://td-3.org/bundle Bundle endpoint profile : https_spiffe Endpoint SPIFFE ID : spiffe://td-3.org/bundle `, + expOutJSON: `{ + "results": [ + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-1.org", + "bundle_endpoint_url": "https://td-1.org/bundle", + "https_web": {}, + "trust_domain_bundle": null + } + }, + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-2.org", + "bundle_endpoint_url": "https://td-2.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://other.org/bundle" + }, + "trust_domain_bundle": null + } + }, + { + "status": { + "code": 0, + "message": "OK" + }, + "federation_relationship": { + "trust_domain": "td-3.org", + "bundle_endpoint_url": "https://td-3.org/bundle", + "https_spiffe": { + "endpoint_spiffe_id": "spiffe://td-3.org/bundle" + }, + "trust_domain_bundle": { + "trust_domain": "td-3.org", + "x509_authorities": [ + { + "asn1": "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyvsCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09Xmakw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylAdZglS5kKnYigmwDh+/U=" + } + ], + "jwt_authorities": [], + "refresh_hint": "0", + "sequence_number": "0" + } + } + } + ] +}`, }, { - name: "Loading federation relationships from JSON file: invalid path", - args: []string{"-data", "somePath"}, - expErr: fmt.Sprintf("Error: open somePath: %s\n", spiretest.FileNotFound()), + name: "Loading federation relationships from JSON file: invalid path", + args: []string{"-data", "somePath"}, + expErrPretty: fmt.Sprintf("Error: open somePath: %s\n", spiretest.FileNotFound()), + expErrJSON: fmt.Sprintf("Error: open somePath: %s\n", spiretest.FileNotFound()), }, { - name: "Loading federation relationships from JSON file: no a json", - args: []string{"-data", bundlePath}, - expErr: "Error: failed to parse JSON: invalid character '-' in numeric literal\n", + name: "Loading federation relationships from JSON file: no a json", + args: []string{"-data", bundlePath}, + expErrPretty: "Error: failed to parse JSON: invalid character '-' in numeric literal\n", + expErrJSON: "Error: failed to parse JSON: invalid character '-' in numeric literal\n", }, { - name: "Loading federation relationships from JSON file: invalid relationship", - args: []string{"-data", jsonDataInvalidRelationship}, - expErr: "Error: could not parse item 0: trust domain is required\n", + name: "Loading federation relationships from JSON file: invalid relationship", + args: []string{"-data", jsonDataInvalidRelationship}, + expErrPretty: "Error: could not parse item 0: trust domain is required\n", + expErrJSON: "Error: could not parse item 0: trust domain is required\n", }, { - name: "Loading federation relationships from JSON file: multiple flags", - args: []string{"-data", jsonDataInvalidRelationship, "-bundleEndpointURL", "https://td-1.org/bundle"}, - expErr: "Error: cannot use other flags to specify relationship fields when 'data' flag is set\n", + name: "Loading federation relationships from JSON file: multiple flags", + args: []string{"-data", jsonDataInvalidRelationship, "-bundleEndpointURL", "https://td-1.org/bundle"}, + expErrPretty: "Error: cannot use other flags to specify relationship fields when 'data' flag is set\n", + expErrJSON: "Error: cannot use other flags to specify relationship fields when 'data' flag is set\n", }, } { - tt := tt - t.Run(tt.name, func(t *testing.T) { - test := setupTest(t, newUpdateCommand) - test.server.err = tt.serverErr - test.server.expectUpdateReq = tt.expReq - test.server.updateResp = tt.fakeResp + for _, format := range availableFormats { + t.Run(fmt.Sprintf("%s using %s format", tt.name, format), func(t *testing.T) { + test := setupTest(t, newUpdateCommand) + test.server.err = tt.serverErr + test.server.expectUpdateReq = tt.expReq + test.server.updateResp = tt.fakeResp + args := tt.args + args = append(args, "-output", format) - rc := test.client.Run(test.args(tt.args...)) - if tt.expErr != "" { - require.Equal(t, 1, rc) - require.Equal(t, tt.expErr, test.stderr.String()) - return - } + rc := test.client.Run(test.args(args...)) - require.Equal(t, 0, rc) - require.Equal(t, tt.expOut, test.stdout.String()) - }) + if tt.expErrPretty != "" && format == "pretty" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrPretty, test.stderr.String()) + return + } + if tt.expErrJSON != "" && format == "json" { + require.Equal(t, 1, rc) + require.Equal(t, tt.expErrJSON, test.stderr.String()) + return + } + require.Equal(t, 0, rc) + requireOutputBasedOnFormat(t, format, test.stdout.String(), tt.expOutPretty, tt.expOutJSON) + }) + } } } diff --git a/cmd/spire-server/cli/federation/util_posix_test.go b/cmd/spire-server/cli/federation/util_posix_test.go new file mode 100644 index 0000000000..61549af1be --- /dev/null +++ b/cmd/spire-server/cli/federation/util_posix_test.go @@ -0,0 +1,77 @@ +//go:build !windows +// +build !windows + +package federation + +const ( + createUsage = `Usage of federation create: + -bundleEndpointProfile string + Endpoint profile type (either "https_web" or "https_spiffe") + -bundleEndpointURL string + URL of the SPIFFE bundle endpoint that provides the trust bundle (must use the HTTPS protocol) + -data string + Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin. + -endpointSpiffeID string + SPIFFE ID of the SPIFFE bundle endpoint server. Only used for 'spiffe' profile. + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") + -trustDomain string + Name of the trust domain to federate with (e.g., example.org) + -trustDomainBundleFormat string + The format of the bundle data (optional). Either "pem" or "spiffe". (default "pem") + -trustDomainBundlePath string + Path to the trust domain bundle data (optional). +` + deleteUsage = `Usage of federation delete: + -id string + SPIFFE ID of the trust domain + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + listUsage = `Usage of federation list: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + refreshUsage = `Usage of federation refresh: + -id string + SPIFFE ID of the trust domain + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") +` + showUsage = `Usage of federation show: + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") + -trustDomain string + The trust domain name of the federation relationship to show +` + updateUsage = `Usage of federation update: + -bundleEndpointProfile string + Endpoint profile type (either "https_web" or "https_spiffe") + -bundleEndpointURL string + URL of the SPIFFE bundle endpoint that provides the trust bundle (must use the HTTPS protocol) + -data string + Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin. + -endpointSpiffeID string + SPIFFE ID of the SPIFFE bundle endpoint server. Only used for 'spiffe' profile. + -output value + Desired output format (pretty, json); default: pretty. + -socketPath string + Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock") + -trustDomain string + Name of the trust domain to federate with (e.g., example.org) + -trustDomainBundleFormat string + The format of the bundle data (optional). Either "pem" or "spiffe". (default "pem") + -trustDomainBundlePath string + Path to the trust domain bundle data (optional). +` +) diff --git a/cmd/spire-server/cli/federation/util_windows_test.go b/cmd/spire-server/cli/federation/util_windows_test.go new file mode 100644 index 0000000000..3a694743dc --- /dev/null +++ b/cmd/spire-server/cli/federation/util_windows_test.go @@ -0,0 +1,77 @@ +//go:build windows +// +build windows + +package federation + +const ( + createUsage = `Usage of federation create: + -bundleEndpointProfile string + Endpoint profile type (either "https_web" or "https_spiffe") + -bundleEndpointURL string + URL of the SPIFFE bundle endpoint that provides the trust bundle (must use the HTTPS protocol) + -data string + Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin. + -endpointSpiffeID string + SPIFFE ID of the SPIFFE bundle endpoint server. Only used for 'spiffe' profile. + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. + -trustDomain string + Name of the trust domain to federate with (e.g., example.org) + -trustDomainBundleFormat string + The format of the bundle data (optional). Either "pem" or "spiffe". (default "pem") + -trustDomainBundlePath string + Path to the trust domain bundle data (optional). +` + deleteUsage = `Usage of federation delete: + -id string + SPIFFE ID of the trust domain + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + listUsage = `Usage of federation list: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + refreshUsage = `Usage of federation refresh: + -id string + SPIFFE ID of the trust domain + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. +` + showUsage = `Usage of federation show: + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. + -trustDomain string + The trust domain name of the federation relationship to show +` + updateUsage = `Usage of federation update: + -bundleEndpointProfile string + Endpoint profile type (either "https_web" or "https_spiffe") + -bundleEndpointURL string + URL of the SPIFFE bundle endpoint that provides the trust bundle (must use the HTTPS protocol) + -data string + Path to a file containing federation relationships in JSON format (optional). If set to '-', read the JSON from stdin. + -endpointSpiffeID string + SPIFFE ID of the SPIFFE bundle endpoint server. Only used for 'spiffe' profile. + -namedPipeName string + Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api") + -output value + Desired output format (pretty, json); default: pretty. + -trustDomain string + Name of the trust domain to federate with (e.g., example.org) + -trustDomainBundleFormat string + The format of the bundle data (optional). Either "pem" or "spiffe". (default "pem") + -trustDomainBundlePath string + Path to the trust domain bundle data (optional). +` +) diff --git a/cmd/spire-server/cli/jwt/mint.go b/cmd/spire-server/cli/jwt/mint.go index 26bb69bd8f..3dd5e5fce8 100644 --- a/cmd/spire-server/cli/jwt/mint.go +++ b/cmd/spire-server/cli/jwt/mint.go @@ -5,7 +5,6 @@ import ( "errors" "flag" "fmt" - "os" "time" "github.com/mitchellh/cli" @@ -14,6 +13,7 @@ import ( "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/spiffe/spire/cmd/spire-server/util" common_cli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/diskutil" "gopkg.in/square/go-jose.v2/jwt" ) @@ -81,7 +81,7 @@ func (c *mintCommand) Run(ctx context.Context, env *common_cli.Env, serverClient // Save in file tokenPath := env.JoinPath(c.write) - if err := os.WriteFile(tokenPath, []byte(token), 0600); err != nil { + if err := diskutil.WritePrivateFile(tokenPath, []byte(token)); err != nil { return fmt.Errorf("unable to write token: %w", err) } return env.Printf("JWT-SVID written to %s\n", tokenPath) diff --git a/cmd/spire-server/cli/jwt/mint_test.go b/cmd/spire-server/cli/jwt/mint_test.go index 3342da5ea1..8983b553ce 100644 --- a/cmd/spire-server/cli/jwt/mint_test.go +++ b/cmd/spire-server/cli/jwt/mint_test.go @@ -203,7 +203,7 @@ func TestMintRun(t *testing.T) { }, }, write: "/", - stderr: fmt.Sprintf("Error: unable to write token: open %s: is a directory\n", dir), + stderr: "Error: unable to write token", }, { name: "malformed token", @@ -290,7 +290,7 @@ func TestMintRun(t *testing.T) { code := cmd.Run(args) assert.Equal(t, tt.code, code, "exit code does not match") - assert.Equal(t, tt.stderr, stderr.String(), "stderr does not match") + assert.Contains(t, stderr.String(), tt.stderr, "stderr does not match") req := server.lastMintJWTSVIDRequest() if tt.noRequestExpected { diff --git a/cmd/spire-server/cli/run/run.go b/cmd/spire-server/cli/run/run.go index 5dd506b8d3..f0bc3a2015 100644 --- a/cmd/spire-server/cli/run/run.go +++ b/cmd/spire-server/cli/run/run.go @@ -65,28 +65,27 @@ type Config struct { } type serverConfig struct { - AdminIDs []string `hcl:"admin_ids"` - AgentTTL string `hcl:"agent_ttl"` - AuditLogEnabled bool `hcl:"audit_log_enabled"` - BindAddress string `hcl:"bind_address"` - BindPort int `hcl:"bind_port"` - CAKeyType string `hcl:"ca_key_type"` - CASubject *caSubjectConfig `hcl:"ca_subject"` - CATTL string `hcl:"ca_ttl"` - DataDir string `hcl:"data_dir"` - DefaultSVIDTTL string `hcl:"default_svid_ttl"` - Experimental experimentalConfig `hcl:"experimental"` - Federation *federationConfig `hcl:"federation"` - JWTIssuer string `hcl:"jwt_issuer"` - JWTKeyType string `hcl:"jwt_key_type"` - LogFile string `hcl:"log_file"` - LogLevel string `hcl:"log_level"` - LogFormat string `hcl:"log_format"` - // Deprecated: remove in SPIRE 1.6.0 - OmitX509SVIDUID *bool `hcl:"omit_x509svid_uid"` - RateLimit rateLimitConfig `hcl:"ratelimit"` - SocketPath string `hcl:"socket_path"` - TrustDomain string `hcl:"trust_domain"` + AdminIDs []string `hcl:"admin_ids"` + AgentTTL string `hcl:"agent_ttl"` + AuditLogEnabled bool `hcl:"audit_log_enabled"` + BindAddress string `hcl:"bind_address"` + BindPort int `hcl:"bind_port"` + CAKeyType string `hcl:"ca_key_type"` + CASubject *caSubjectConfig `hcl:"ca_subject"` + CATTL string `hcl:"ca_ttl"` + DataDir string `hcl:"data_dir"` + DefaultX509SVIDTTL string `hcl:"default_x509_svid_ttl"` + DefaultJWTSVIDTTL string `hcl:"default_jwt_svid_ttl"` + Experimental experimentalConfig `hcl:"experimental"` + Federation *federationConfig `hcl:"federation"` + JWTIssuer string `hcl:"jwt_issuer"` + JWTKeyType string `hcl:"jwt_key_type"` + LogFile string `hcl:"log_file"` + LogLevel string `hcl:"log_level"` + LogFormat string `hcl:"log_format"` + RateLimit rateLimitConfig `hcl:"ratelimit"` + SocketPath string `hcl:"socket_path"` + TrustDomain string `hcl:"trust_domain"` ConfigPath string ExpandEnv bool @@ -97,6 +96,10 @@ type serverConfig struct { ProfilingFreq int `hcl:"profiling_freq"` ProfilingNames []string `hcl:"profiling_names"` + // Deprecated: remove in SPIRE 1.6.0 + DefaultSVIDTTL string `hcl:"default_svid_ttl"` + OmitX509SVIDUID *bool `hcl:"omit_x509svid_uid"` + UnusedKeys []string `hcl:",unusedKeys"` } @@ -165,12 +168,13 @@ type rateLimitConfig struct { UnusedKeys []string `hcl:",unusedKeys"` } -func NewRunCommand(logOptions []log.Option, allowUnknownConfig bool) cli.Command { - return newRunCommand(common_cli.DefaultEnv, logOptions, allowUnknownConfig) +func NewRunCommand(ctx context.Context, logOptions []log.Option, allowUnknownConfig bool) cli.Command { + return newRunCommand(ctx, common_cli.DefaultEnv, logOptions, allowUnknownConfig) } -func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command { +func newRunCommand(ctx context.Context, env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command { return &Command{ + ctx: ctx, env: env, logOptions: logOptions, allowUnknownConfig: allowUnknownConfig, @@ -179,6 +183,7 @@ func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownCon // Run Command struct type Command struct { + ctx context.Context logOptions []log.Option env *common_cli.Env allowUnknownConfig bool @@ -238,7 +243,11 @@ func (cmd *Command) Run(args []string) int { s := server.New(*c) - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + ctx := cmd.ctx + if ctx == nil { + ctx = context.Background() + } + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer stop() err = s.Run(ctx) @@ -467,12 +476,45 @@ func NewServerConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool sc.AgentTTL = ttl } - if c.Server.DefaultSVIDTTL != "" { + switch { + case c.Server.DefaultX509SVIDTTL != "": + ttl, err := time.ParseDuration(c.Server.DefaultX509SVIDTTL) + if err != nil { + return nil, fmt.Errorf("could not parse default X509 SVID ttl %q: %w", c.Server.DefaultX509SVIDTTL, err) + } + sc.X509SVIDTTL = ttl + + if sc.X509SVIDTTL != 0 && c.Server.DefaultSVIDTTL != "" { + logger.Warnf("both default_x509_svid_ttl and default_svid_ttl are configured; default_x509_svid_ttl (%s) will be used for X509-SVIDs", c.Server.DefaultX509SVIDTTL) + } + case c.Server.DefaultSVIDTTL != "": + logger.Warn("field default_svid_ttl is deprecated; consider using default_x509_svid_ttl and default_jwt_svid_ttl instead") + ttl, err := time.ParseDuration(c.Server.DefaultSVIDTTL) if err != nil { return nil, fmt.Errorf("could not parse default SVID ttl %q: %w", c.Server.DefaultSVIDTTL, err) } - sc.SVIDTTL = ttl + sc.X509SVIDTTL = ttl + default: + // If neither new nor deprecated config value is set, then use hard-coded default TTL + // Note, due to back-compat issues we cannot set this default inside defaultConfig() function + sc.X509SVIDTTL = ca.DefaultX509SVIDTTL + } + + if c.Server.DefaultJWTSVIDTTL != "" { + ttl, err := time.ParseDuration(c.Server.DefaultJWTSVIDTTL) + if err != nil { + return nil, fmt.Errorf("could not parse default JWT SVID ttl %q: %w", c.Server.DefaultJWTSVIDTTL, err) + } + sc.JWTSVIDTTL = ttl + + if sc.JWTSVIDTTL != 0 && c.Server.DefaultSVIDTTL != "" { + logger.Warnf("both default_jwt_svid_ttl and default_svid_ttl are configured; default_jwt_svid_ttl (%s) will be used for JWT-SVIDs", c.Server.DefaultJWTSVIDTTL) + } + } else { + // If not set using new field then use hard-coded default TTL + // Note, due to back-compat issues we cannot set this default inside defaultConfig() function + sc.JWTSVIDTTL = ca.DefaultJWTSVIDTTL } if c.Server.CATTL != "" { @@ -485,43 +527,57 @@ func NewServerConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool // If the configured TTLs can lead to surprises, then do our best to log an // accurate message and guide the user to resolution - if !hasCompatibleTTLs(sc.CATTL, sc.SVIDTTL) { - msgCATTLTooSmall := fmt.Sprintf( - "The default_svid_ttl is too high for the configured ca_ttl value. "+ - "SVIDs with shorter lifetimes may be issued. "+ - "Please set the default_svid_ttl to %v or less, or the ca_ttl to %v or more, "+ - "to guarantee the full default_svid_ttl lifetime when CA rotations are scheduled.", - printMaxSVIDTTL(sc.CATTL), printMinCATTL(sc.SVIDTTL), - ) - msgSVIDTTLTooLargeAndCATTLTooSmall := fmt.Sprintf( - "The default_svid_ttl is too high and the ca_ttl is too low. "+ - "SVIDs with shorter lifetimes may be issued. "+ - "Please set the default_svid_ttl to %v or less, and the ca_ttl to %v or more, "+ - "to guarantee the full default_svid_ttl lifetime when CA rotations are scheduled.", - printDuration(ca.MaxSVIDTTL()), printMinCATTL(ca.MaxSVIDTTL()), - ) - msgSVIDTTLTooLarge := fmt.Sprintf( - "The default_svid_ttl is too high. "+ - "SVIDs with shorter lifetimes may be issued. "+ - "Please set the default_svid_ttl to %v or less "+ - "to guarantee the full default_svid_ttl lifetime when CA rotations are scheduled.", - printMaxSVIDTTL(sc.CATTL), - ) + ttlChecks := []struct { + name string + ttl time.Duration + }{ + { + name: "default_x509_svid_ttl (or deprecated default_svid_ttl)", + ttl: sc.X509SVIDTTL, + }, + { + name: "default_jwt_svid_ttl", + ttl: sc.JWTSVIDTTL, + }, + } - switch { - case sc.SVIDTTL < ca.MaxSVIDTTL(): - // The SVID TTL is smaller than our cap, but the CA TTL - // is not large enough to accommodate it - sc.Log.Warn(msgCATTLTooSmall) - case sc.CATTL < ca.MinCATTLForSVIDTTL(ca.MaxSVIDTTL()): - // The SVID TTL is larger than our cap, it needs to be - // decreased no matter what. Additionally, the CA TTL is - // too small to accommodate the maximum SVID TTL. - sc.Log.Warn(msgSVIDTTLTooLargeAndCATTLTooSmall) - default: - // The SVID TTL is larger than our cap and needs to be - // decreased. - sc.Log.Warn(msgSVIDTTLTooLarge) + for _, ttlCheck := range ttlChecks { + if !hasCompatibleTTL(sc.CATTL, ttlCheck.ttl) { + var message string + + switch { + case ttlCheck.ttl < ca.MaxSVIDTTL(): + // TTL is smaller than our cap, but the CA TTL + // is not large enough to accommodate it + message = fmt.Sprintf("%s is too high for the configured "+ + "ca_ttl value. SVIDs with shorter lifetimes may "+ + "be issued. Please set %s to %v or less, or the ca_ttl "+ + "to %v or more, to guarantee the full %s lifetime "+ + "when CA rotations are scheduled.", + ttlCheck.name, ttlCheck.name, printMaxSVIDTTL(sc.CATTL), printMinCATTL(ttlCheck.ttl), ttlCheck.name, + ) + case sc.CATTL < ca.MinCATTLForSVIDTTL(ca.MaxSVIDTTL()): + // TTL is larger than our cap, it needs to be + // decreased no matter what. Additionally, the CA TTL is + // too small to accommodate the maximum SVID TTL. + message = fmt.Sprintf("%s is too high and "+ + "the ca_ttl is too low. SVIDs with shorter lifetimes "+ + "may be issued. Please set %s to %v or less, and the "+ + "ca_ttl to %v or more, to guarantee the full %s "+ + "lifetime when CA rotations are scheduled.", + ttlCheck.name, ttlCheck.name, printDuration(ca.MaxSVIDTTL()), printMinCATTL(ca.MaxSVIDTTL()), ttlCheck.name, + ) + default: + // TTL is larger than our cap and needs to be + // decreased. + message = fmt.Sprintf("%s is too high. SVIDs with shorter "+ + "lifetimes may be issued. Please set %s to %v or less "+ + "to guarantee the full %s lifetime when CA rotations "+ + "are scheduled.", + ttlCheck.name, ttlCheck.name, printMaxSVIDTTL(sc.CATTL), ttlCheck.name, + ) + } + sc.Log.Warn(message) } } @@ -791,13 +847,12 @@ func checkForUnknownConfig(c *Config, l logrus.FieldLogger) (err error) { func defaultConfig() *Config { return &Config{ Server: &serverConfig{ - BindAddress: "0.0.0.0", - BindPort: 8081, - CATTL: ca.DefaultCATTL.String(), - LogLevel: defaultLogLevel, - LogFormat: log.DefaultFormat, - DefaultSVIDTTL: ca.DefaultX509SVIDTTL.String(), - Experimental: experimentalConfig{}, + BindAddress: "0.0.0.0", + BindPort: 8081, + CATTL: ca.DefaultCATTL.String(), + LogLevel: defaultLogLevel, + LogFormat: log.DefaultFormat, + Experimental: experimentalConfig{}, }, } } @@ -817,11 +872,12 @@ func keyTypeFromString(s string) (keymanager.KeyType, error) { } } -// hasCompatibleTTLs checks if we can guarantee the configured SVID TTL given the -// configurd CA TTL. If we detect that a new SVIDs TTL may be cut short due to -// a scheduled CA rotation, this function will return false. -func hasCompatibleTTLs(caTTL, svidTTL time.Duration) bool { - return ca.MaxSVIDTTLForCATTL(caTTL) >= svidTTL +// hasCompatibleTTL checks if we can guarantee the configured SVID TTL given the +// configurd CA TTL. If we detect that a new SVID TTL may be cut short due to +// a scheduled CA rotation, this function will return false. This method should +// be called for each SVID TTL we may use +func hasCompatibleTTL(caTTL time.Duration, svidTTL time.Duration) bool { + return svidTTL <= ca.MaxSVIDTTLForCATTL(caTTL) } // printMaxSVIDTTL calculates the display string for a sufficiently short SVID TTL diff --git a/cmd/spire-server/cli/run/run_posix_test.go b/cmd/spire-server/cli/run/run_posix_test.go index 22a0a896a2..5b2c872045 100644 --- a/cmd/spire-server/cli/run/run_posix_test.go +++ b/cmd/spire-server/cli/run/run_posix_test.go @@ -4,18 +4,173 @@ package run import ( + "bytes" + "fmt" "os" + "strings" + "syscall" "testing" + "time" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/fflag" + "github.com/spiffe/spire/pkg/common/log" "github.com/spiffe/spire/pkg/server" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( - configFile = "../../../../test/fixture/config/server_good_posix.conf" + configFile = "../../../../test/fixture/config/server_good_posix.conf" + startConfigFile = "../../../../test/fixture/config/server_run_start_posix.conf" + crashConfigFile = "../../../../test/fixture/config/server_run_crash_posix.conf" ) +func TestCommand_Run(t *testing.T) { + testTempDir := t.TempDir() + testLogFile := testTempDir + "/spire-server.log" + + type fields struct { + logOptions []log.Option + env *commoncli.Env + allowUnknownConfig bool + } + type args struct { + args []string + killServerOnStart bool + } + type want struct { + code int + dataDirCreated string + stderrContent string + } + tests := []struct { + name string + fields fields + args args + configLoaded bool + want want + }{ + { + name: "don't create any dir when error loading nonexistent config", + args: args{ + args: []string{}, + }, + fields: fields{ + logOptions: []log.Option{log.WithOutputFile(testLogFile)}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + configLoaded: false, + want: want{ + code: 1, + stderrContent: "could not find config file", + }, + }, + { + name: "don't create any dir when error loading invalid config", + args: args{ + args: []string{ + "-config", startConfigFile, + "-namedPipeName", "\\spire-agent\\public\\api", + }, + }, + fields: fields{ + logOptions: []log.Option{log.WithOutputFile(testLogFile)}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + configLoaded: false, + want: want{ + code: 1, + stderrContent: "flag provided but not defined: -namedPipeName", + }, + }, + { + name: "create data dir when config is loaded and server crashes", + args: args{ + args: []string{ + "-config", crashConfigFile, + "-dataDir", fmt.Sprintf("%s/crash/data", testTempDir), + "-expandEnv", "true", + }, + }, + fields: fields{ + logOptions: []log.Option{log.WithOutputFile(testLogFile)}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + configLoaded: true, + want: want{ + code: 1, + dataDirCreated: fmt.Sprintf("%s/crash/data", testTempDir), + }, + }, + { + name: "create data dir when config is loaded and server stops", + args: args{ + args: []string{ + "-config", startConfigFile, + "-dataDir", fmt.Sprintf("%s/data", testTempDir), + "-expandEnv", "true", + }, + killServerOnStart: true, + }, + fields: fields{ + logOptions: []log.Option{log.WithOutputFile(testLogFile)}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + configLoaded: true, + want: want{ + code: 0, + dataDirCreated: fmt.Sprintf("%s/data", testTempDir), + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + _ = fflag.Unload() + require.NoError(t, os.Setenv("SPIRE_SERVER_TEST_DATA_CONNECTION", fmt.Sprintf("%s/data/datastore.sqlite3", testTempDir))) + os.Remove(testLogFile) + + cmd := &Command{ + logOptions: testCase.fields.logOptions, + env: testCase.fields.env, + allowUnknownConfig: testCase.fields.allowUnknownConfig, + } + + if testCase.args.killServerOnStart { + killServerOnStart(t, testLogFile) + } + + code := cmd.Run(testCase.args.args) + + assert.Equal(t, testCase.want.code, code) + if testCase.want.stderrContent == "" { + assert.Empty(t, testCase.fields.env.Stderr.(*bytes.Buffer).String()) + } else { + assert.Contains(t, testCase.fields.env.Stderr.(*bytes.Buffer).String(), testCase.want.stderrContent) + } + if testCase.want.dataDirCreated != "" { + assert.DirExistsf(t, testCase.want.dataDirCreated, "data directory should be created") + currentUmask := syscall.Umask(0) + assert.Equalf(t, currentUmask, 0027, "spire-server process should have been created with 0027 umask") + } else { + assert.NoDirExistsf(t, testCase.want.dataDirCreated, "data directory should not be created") + } + }) + } +} + func TestParseFlagsGood(t *testing.T) { c, err := parseFlags("run", []string{ "-bindAddress=127.0.0.1", @@ -30,6 +185,37 @@ func TestParseFlagsGood(t *testing.T) { assert.Equal(t, c.LogLevel, "INFO") } +func killServerOnStart(t *testing.T, testLogFile string) { + go func() { + serverStartWaitingTimeout := 10 * time.Second + serverStartWaitingInterval := 100 * time.Millisecond + ticker := time.NewTicker(serverStartWaitingInterval) + timer := time.NewTimer(serverStartWaitingTimeout) + waitingLoop: + for { + select { + case <-timer.C: + panic("server did not start in time") + case <-ticker.C: + logs, err := os.ReadFile(testLogFile) + + if err != nil { + continue + } + if strings.Contains(string(logs), "Starting Server APIs") { + timer.Stop() + break waitingLoop + } + } + } + + err := syscall.Kill(syscall.Getpid(), syscall.SIGINT) + if err != nil { + t.Errorf("Failed to kill process: %v", err) + } + }() +} + func mergeInputCasesOS(t *testing.T) []mergeInputCase { return []mergeInputCase{ { diff --git a/cmd/spire-server/cli/run/run_test.go b/cmd/spire-server/cli/run/run_test.go index 044e057c62..d4fa675a3b 100644 --- a/cmd/spire-server/cli/run/run_test.go +++ b/cmd/spire-server/cli/run/run_test.go @@ -355,12 +355,34 @@ func TestMergeInput(t *testing.T) { msg: "default_svid_ttl should be configurable by file", fileInput: func(c *Config) { c.Server.DefaultSVIDTTL = "1h" + c.Server.DefaultX509SVIDTTL = "" + c.Server.DefaultJWTSVIDTTL = "" }, cliFlags: []string{}, test: func(t *testing.T, c *Config) { require.Equal(t, "1h", c.Server.DefaultSVIDTTL) }, }, + { + msg: "default_x509_svid_ttl should be configurable by file", + fileInput: func(c *Config) { + c.Server.DefaultX509SVIDTTL = "2h" + }, + cliFlags: []string{}, + test: func(t *testing.T, c *Config) { + require.Equal(t, "2h", c.Server.DefaultX509SVIDTTL) + }, + }, + { + msg: "default_jwt_svid_ttl should be configurable by file", + fileInput: func(c *Config) { + c.Server.DefaultJWTSVIDTTL = "3h" + }, + cliFlags: []string{}, + test: func(t *testing.T, c *Config) { + require.Equal(t, "3h", c.Server.DefaultJWTSVIDTTL) + }, + }, { msg: "trust_domain should not have a default value", fileInput: func(c *Config) {}, @@ -619,7 +641,25 @@ func TestNewServerConfig(t *testing.T) { c.Server.DefaultSVIDTTL = "1m" }, test: func(t *testing.T, c *server.Config) { - require.Equal(t, time.Minute, c.SVIDTTL) + require.Equal(t, time.Minute, c.X509SVIDTTL) + }, + }, + { + msg: "default_x509_svid_ttl is correctly parsed", + input: func(c *Config) { + c.Server.DefaultX509SVIDTTL = "2m" + }, + test: func(t *testing.T, c *server.Config) { + require.Equal(t, 2*time.Minute, c.X509SVIDTTL) + }, + }, + { + msg: "default_jwt_svid_ttl is correctly parsed", + input: func(c *Config) { + c.Server.DefaultJWTSVIDTTL = "3m" + }, + test: func(t *testing.T, c *server.Config) { + require.Equal(t, 3*time.Minute, c.JWTSVIDTTL) }, }, { @@ -627,6 +667,28 @@ func TestNewServerConfig(t *testing.T) { expectError: true, input: func(c *Config) { c.Server.DefaultSVIDTTL = "b" + c.Server.DefaultX509SVIDTTL = "" + c.Server.DefaultJWTSVIDTTL = "" + }, + test: func(t *testing.T, c *server.Config) { + require.Nil(t, c) + }, + }, + { + msg: "invalid default_x509_svid_ttl returns an error", + expectError: true, + input: func(c *Config) { + c.Server.DefaultX509SVIDTTL = "b" + }, + test: func(t *testing.T, c *server.Config) { + require.Nil(t, c) + }, + }, + { + msg: "invalid default_jwt_svid_ttl returns an error", + expectError: true, + input: func(c *Config) { + c.Server.DefaultJWTSVIDTTL = "b" }, test: func(t *testing.T, c *server.Config) { require.Nil(t, c) @@ -1382,64 +1444,234 @@ func TestLogOptions(t *testing.T) { func TestHasCompatibleTTLs(t *testing.T) { cases := []struct { - msg string - caTTL time.Duration - svidTTL time.Duration - hasCompatibleTTLs bool + msg string + caTTL time.Duration + svidTTL time.Duration + x509SvidTTL time.Duration + jwtSvidTTL time.Duration + hasCompatibleSvidTTL bool + hasCompatibleX509SvidTTL bool + hasCompatibleJwtSvidTTL bool }{ { - msg: "Both values are default values", - caTTL: 0, - svidTTL: 0, - hasCompatibleTTLs: true, - }, - { - msg: "ca_ttl is large enough for the default SVID TTL", - caTTL: time.Hour * 7, - svidTTL: 0, - hasCompatibleTTLs: true, - }, - { - msg: "ca_ttl is not large enough for the default SVID TTL", - caTTL: time.Minute * 1, - svidTTL: 0, - hasCompatibleTTLs: false, - }, - { - msg: "default_svid_ttl is small enough for the default CA TTL", - caTTL: 0, - svidTTL: time.Hour * 3, - hasCompatibleTTLs: true, - }, - { - msg: "default_svid_ttl is not small enough for the default CA TTL", - caTTL: 0, - svidTTL: time.Hour * 24, - hasCompatibleTTLs: false, - }, - { - msg: "default_svid_ttl is small enough for the configured CA TTL", - caTTL: time.Hour * 24, - svidTTL: time.Hour * 1, - hasCompatibleTTLs: true, - }, - { - msg: "default_svid_ttl is not small enough for the configured CA TTL", - caTTL: time.Hour * 24, - svidTTL: time.Hour * 23, - hasCompatibleTTLs: false, - }, - { - msg: "default_svid_ttl is larger than the configured CA TTL", - caTTL: time.Hour * 24, - svidTTL: time.Hour * 25, - hasCompatibleTTLs: false, - }, - { - msg: "default_svid_ttl is small enough for the configured CA TTL but larger than the max", - caTTL: time.Hour * 24 * 7 * 4 * 6, // Six months - svidTTL: time.Hour * 24 * 7 * 2, // Two weeks - hasCompatibleTTLs: false, + msg: "All values are default values", + caTTL: 0, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "ca_ttl is large enough for all default SVID TTL", + caTTL: time.Hour * 7, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "ca_ttl is not large enough for the default SVID TTL", + caTTL: time.Minute * 1, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: false, + hasCompatibleX509SvidTTL: false, + hasCompatibleJwtSvidTTL: false, + }, + { + msg: "default_svid_ttl is small enough for the default CA TTL", + caTTL: 0, + svidTTL: time.Hour * 3, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_svid_ttl is not small enough for the default CA TTL", + caTTL: 0, + svidTTL: time.Hour * 24, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: false, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_svid_ttl is small enough for the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: time.Hour * 1, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_svid_ttl is not small enough for the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: time.Hour * 23, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: false, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_svid_ttl is larger than the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: time.Hour * 25, + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: false, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_svid_ttl is small enough for the configured CA TTL but larger than the max", + caTTL: time.Hour * 24 * 7 * 4 * 6, // Six months + svidTTL: time.Hour * 24 * 7 * 2, // Two weeks + x509SvidTTL: 0, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: false, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_x509_svid_ttl is small enough for the default CA TTL", + caTTL: 0, + svidTTL: 0, + x509SvidTTL: time.Hour * 3, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_x509_svid_ttl is not small enough for the default CA TTL", + caTTL: 0, + svidTTL: 0, + x509SvidTTL: time.Hour * 24, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: false, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_x509_svid_ttl is small enough for the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: 0, + x509SvidTTL: time.Hour * 1, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_x509_svid_ttl is not small enough for the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: 0, + x509SvidTTL: time.Hour * 23, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: false, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_x509_svid_ttl is larger than the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: 0, + x509SvidTTL: time.Hour * 25, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: false, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_x509_svid_ttl is small enough for the configured CA TTL but larger than the max", + caTTL: time.Hour * 24 * 7 * 4 * 6, // Six months + svidTTL: 0, + x509SvidTTL: time.Hour * 24 * 7 * 2, // Two weeks, + jwtSvidTTL: 0, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: false, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_jwt_svid_ttl is small enough for the default CA TTL", + caTTL: 0, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: time.Hour * 3, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_jwt_svid_ttl is not small enough for the default CA TTL", + caTTL: 0, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: time.Hour * 24, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: false, + }, + { + msg: "default_jwt_svid_ttl is small enough for the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: time.Hour * 1, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, + }, + { + msg: "default_jwt_svid_ttl is not small enough for the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: time.Hour * 23, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: false, + }, + { + msg: "default_jwt_svid_ttl is larger than the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: time.Hour * 25, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: false, + }, + { + msg: "default_jwt_svid_ttl is small enough for the configured CA TTL but larger than the max", + caTTL: time.Hour * 24 * 7 * 4 * 6, // Six months + svidTTL: 0, + x509SvidTTL: 0, + jwtSvidTTL: time.Hour * 24 * 7 * 2, // Two weeks,, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: false, + }, + { + msg: "all default svid_ttls are small enough for the configured CA TTL", + caTTL: time.Hour * 24, + svidTTL: time.Hour * 1, + x509SvidTTL: time.Hour * 1, + jwtSvidTTL: time.Hour * 1, + hasCompatibleSvidTTL: true, + hasCompatibleX509SvidTTL: true, + hasCompatibleJwtSvidTTL: true, }, } @@ -1451,9 +1683,17 @@ func TestHasCompatibleTTLs(t *testing.T) { if testCase.svidTTL == 0 { testCase.svidTTL = ca.DefaultX509SVIDTTL } + if testCase.x509SvidTTL == 0 { + testCase.x509SvidTTL = ca.DefaultX509SVIDTTL + } + if testCase.jwtSvidTTL == 0 { + testCase.jwtSvidTTL = ca.DefaultJWTSVIDTTL + } t.Run(testCase.msg, func(t *testing.T) { - require.Equal(t, testCase.hasCompatibleTTLs, hasCompatibleTTLs(testCase.caTTL, testCase.svidTTL)) + require.Equal(t, testCase.hasCompatibleSvidTTL, hasCompatibleTTL(testCase.caTTL, testCase.svidTTL)) + require.Equal(t, testCase.hasCompatibleX509SvidTTL, hasCompatibleTTL(testCase.caTTL, testCase.x509SvidTTL)) + require.Equal(t, testCase.hasCompatibleJwtSvidTTL, hasCompatibleTTL(testCase.caTTL, testCase.jwtSvidTTL)) }) } } @@ -1498,39 +1738,85 @@ func TestMaxSVIDTTL(t *testing.T) { func TestMinCATTL(t *testing.T) { for _, v := range []struct { - svidTTL time.Duration - expect string + x509SVIDTTL time.Duration + jwtSVIDTTL time.Duration + expect string }{ { - svidTTL: 10 * time.Second, - expect: "1m", + x509SVIDTTL: 10 * time.Second, + jwtSVIDTTL: 1 * time.Second, + expect: "1m", + }, + { + x509SVIDTTL: 15 * time.Second, + jwtSVIDTTL: 1 * time.Second, + expect: "1m30s", + }, + { + x509SVIDTTL: 10 * time.Minute, + jwtSVIDTTL: 1 * time.Second, + expect: "1h", + }, + { + x509SVIDTTL: 22 * time.Minute, + jwtSVIDTTL: 1 * time.Second, + expect: "2h12m", }, { - svidTTL: 15 * time.Second, - expect: "1m30s", + x509SVIDTTL: 24 * time.Hour, + jwtSVIDTTL: 1 * time.Second, + expect: "144h", }, { - svidTTL: 10 * time.Minute, - expect: "1h", + x509SVIDTTL: 0, + jwtSVIDTTL: 1 * time.Second, + expect: "6h", + }, + + { + x509SVIDTTL: 1 * time.Second, + jwtSVIDTTL: 10 * time.Second, + expect: "1m", + }, + { + x509SVIDTTL: 1 * time.Second, + jwtSVIDTTL: 15 * time.Second, + expect: "1m30s", }, { - svidTTL: 22 * time.Minute, - expect: "2h12m", + x509SVIDTTL: 1 * time.Second, + jwtSVIDTTL: 10 * time.Minute, + expect: "1h", }, { - svidTTL: 24 * time.Hour, - expect: "144h", + x509SVIDTTL: 1 * time.Second, + jwtSVIDTTL: 22 * time.Minute, + expect: "2h12m", }, { - svidTTL: 0, - expect: "6h", + x509SVIDTTL: 1 * time.Second, + jwtSVIDTTL: 24 * time.Hour, + expect: "144h", + }, + { + x509SVIDTTL: 1 * time.Second, + jwtSVIDTTL: 0, + expect: "30m", }, } { - if v.svidTTL == 0 { - v.svidTTL = ca.DefaultX509SVIDTTL + if v.x509SVIDTTL == 0 { + v.x509SVIDTTL = ca.DefaultX509SVIDTTL + } + if v.jwtSVIDTTL == 0 { + v.jwtSVIDTTL = ca.DefaultJWTSVIDTTL } - assert.Equal(t, v.expect, printMinCATTL(v.svidTTL)) + // The expected value is the MinCATTL calculated from the largest of the available TTLs + if v.x509SVIDTTL > v.jwtSVIDTTL { + assert.Equal(t, v.expect, printMinCATTL(v.x509SVIDTTL)) + } else { + assert.Equal(t, v.expect, printMinCATTL(v.jwtSVIDTTL)) + } } } diff --git a/cmd/spire-server/cli/run/run_windows_test.go b/cmd/spire-server/cli/run/run_windows_test.go index 0e9890705f..c421232317 100644 --- a/cmd/spire-server/cli/run/run_windows_test.go +++ b/cmd/spire-server/cli/run/run_windows_test.go @@ -4,9 +4,14 @@ package run import ( + "bytes" + "fmt" "os" "testing" + commoncli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/fflag" + "github.com/spiffe/spire/pkg/common/log" "github.com/spiffe/spire/pkg/common/namedpipe" "github.com/spiffe/spire/pkg/server" "github.com/stretchr/testify/assert" @@ -17,6 +22,118 @@ const ( configFile = "../../../../test/fixture/config/server_good_windows.conf" ) +func TestCommand_Run(t *testing.T) { + testTempDir := t.TempDir() + testDataDir := fmt.Sprintf("%s/data", testTempDir) + + type fields struct { + logOptions []log.Option + env *commoncli.Env + allowUnknownConfig bool + } + type args struct { + args []string + } + type want struct { + code int + stderrContent string + dataDirCreated bool + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "don't create data dir when error loading nonexistent config", + args: args{ + args: []string{}, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + dataDirCreated: false, + stderrContent: "could not find config file", + }, + }, + { + name: "don't create data dir when error loading invalid config", + args: args{ + args: []string{ + "-config", "../../../../test/fixture/config/server_run_windows.conf", + "-socketPath", "unix:///tmp/agent.sock", + }, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + dataDirCreated: false, + stderrContent: "flag provided but not defined: -socketPath", + }, + }, + { + name: "create data dir when config is loaded", + args: args{ + args: []string{ + "-config", "../../../../test/fixture/config/server_run_windows.conf", + "-dataDir", testDataDir, + }, + }, + fields: fields{ + logOptions: []log.Option{}, + env: &commoncli.Env{ + Stderr: new(bytes.Buffer), + Stdout: new(bytes.Buffer), + }, + allowUnknownConfig: false, + }, + want: want{ + code: 1, + dataDirCreated: true, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + _ = fflag.Unload() + os.RemoveAll(testDataDir) + + cmd := &Command{ + logOptions: testCase.fields.logOptions, + env: testCase.fields.env, + allowUnknownConfig: testCase.fields.allowUnknownConfig, + } + + code := cmd.Run(testCase.args.args) + + assert.Equal(t, testCase.want.code, code) + if testCase.want.stderrContent == "" { + assert.Empty(t, testCase.fields.env.Stderr.(*bytes.Buffer).String()) + } else { + assert.Contains(t, testCase.fields.env.Stderr.(*bytes.Buffer).String(), testCase.want.stderrContent) + } + if testCase.want.dataDirCreated { + assert.DirExistsf(t, testDataDir, "data directory should be created") + } else { + assert.NoDirExistsf(t, testDataDir, "data directory should not be created") + } + }) + } +} + func TestParseFlagsGood(t *testing.T) { c, err := parseFlags("run", []string{ "-bindAddress=127.0.0.1", diff --git a/cmd/spire-server/cli/x509/mint.go b/cmd/spire-server/cli/x509/mint.go index 4fd1a27b45..cac497dc42 100644 --- a/cmd/spire-server/cli/x509/mint.go +++ b/cmd/spire-server/cli/x509/mint.go @@ -13,7 +13,6 @@ import ( "flag" "fmt" "net/url" - "os" "time" "github.com/mitchellh/cli" @@ -22,6 +21,7 @@ import ( svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1" "github.com/spiffe/spire/cmd/spire-server/util" common_cli "github.com/spiffe/spire/pkg/common/cli" + "github.com/spiffe/spire/pkg/common/diskutil" ) type generateKeyFunc func() (crypto.Signer, error) @@ -157,21 +157,21 @@ func (c *mintCommand) Run(ctx context.Context, env *common_cli.Env, serverClient keyPath := env.JoinPath(c.write, "key.pem") bundlePath := env.JoinPath(c.write, "bundle.pem") - if err := os.WriteFile(svidPath, svidPEM.Bytes(), 0644); err != nil { // nolint: gosec // expected permission + if err := diskutil.WritePubliclyReadableFile(svidPath, svidPEM.Bytes()); err != nil { return fmt.Errorf("unable to write SVID: %w", err) } if err := env.Printf("X509-SVID written to %s\n", svidPath); err != nil { return err } - if err := os.WriteFile(keyPath, keyPEM.Bytes(), 0600); err != nil { + if err := diskutil.WritePrivateFile(keyPath, keyPEM.Bytes()); err != nil { return fmt.Errorf("unable to write key: %w", err) } if err := env.Printf("Private key written to %s\n", keyPath); err != nil { return err } - if err := os.WriteFile(bundlePath, bundlePEM.Bytes(), 0644); err != nil { // nolint: gosec // expected permission + if err := diskutil.WritePubliclyReadableFile(bundlePath, bundlePEM.Bytes()); err != nil { return fmt.Errorf("unable to write bundle: %w", err) } return env.Printf("Root CAs written to %s\n", bundlePath) diff --git a/cmd/spire-server/main.go b/cmd/spire-server/main.go index 3962cc788b..ac72e48413 100644 --- a/cmd/spire-server/main.go +++ b/cmd/spire-server/main.go @@ -4,8 +4,9 @@ import ( "os" "github.com/spiffe/spire/cmd/spire-server/cli" + "github.com/spiffe/spire/pkg/common/entrypoint" ) func main() { - os.Exit(new(cli.CLI).Run(os.Args[1:])) + os.Exit(entrypoint.NewEntryPoint(new(cli.CLI).Run).Main()) } diff --git a/conf/agent/agent.conf b/conf/agent/agent.conf index 6926baf7b8..cf1fcb353a 100644 --- a/conf/agent/agent.conf +++ b/conf/agent/agent.conf @@ -18,17 +18,8 @@ plugins { directory = "./.data" } } - WorkloadAttestor "k8s" { - plugin_data { - kubelet_read_only_port = "10255" - } - } WorkloadAttestor "unix" { plugin_data { } } - WorkloadAttestor "docker" { - plugin_data { - } - } } diff --git a/conf/agent/agent_full.conf b/conf/agent/agent_full.conf index daa113389e..9ade8ea621 100644 --- a/conf/agent/agent_full.conf +++ b/conf/agent/agent_full.conf @@ -319,7 +319,7 @@ plugins { WorkloadAttestor "k8s" { plugin_data { # kubelet_read_only_port: The kubelet read-only port. This is mutually - # exlusive with kubelet_secure_port. + # exclusive with kubelet_secure_port. kubelet_read_only_port = "10255" # kubelet_secure_port: The kubelet secure port. It defaults to 10250 @@ -359,6 +359,30 @@ plugins { # node_name: The name of the node. Overrides the value obtained by # the environment variable specified by node_name_env. # node_name = "" + + # experimental: Experimental features. + experimental { + # sigstore: sigstore options. Enables signature checking. + # sigstore { + # rekor_url: The URL for the rekor STL Server to use with cosign. Required. + # rekor_url = "https://rekor.sigstore.dev" + + # skip_signature_verification_image_list: List of images that should + # not be verified by cosign. They will receive a default + # sigstore-validation:passed selector, but no other sigstore related selectors. + # skip_signature_verification_image_list = ["sha:image1hash","sha:image2hash"] + + # allowed_subjects_list: Map of subjects that image signatures + # will be checked against, keyed by OIDC Provider URI. + # Signatures from subjects outside this list will be ignored. These should be email addresses. + # allowed_subjects_list { + # "https://accounts.google.com" = ["subject1@example.com","subject2@example.com"] + # } + + # enforce_sct: to be set as false in case of a private deployment not using the public CT + # enforce_sct = true + # } + } } } diff --git a/conf/server/server.conf b/conf/server/server.conf index 0195aaeae4..a21d53b93a 100644 --- a/conf/server/server.conf +++ b/conf/server/server.conf @@ -5,11 +5,6 @@ server { trust_domain = "example.org" data_dir = "./.data" log_level = "DEBUG" - ca_subject { - country = ["US"] - organization = ["SPIFFE"] - common_name = "" - } } plugins { diff --git a/conf/server/server_full.conf b/conf/server/server_full.conf index 06d57bb52b..e5c0e41cbb 100644 --- a/conf/server/server_full.conf +++ b/conf/server/server_full.conf @@ -8,7 +8,7 @@ server { # domain as the server and need not have a corresponding admin registration # entry with the server. # admin_ids = ["spiffe://example.org/my/admin"] - + # bind_address: IP address or DNS name of the SPIRE server. # Default: 0.0.0.0. bind_address = "127.0.0.1" @@ -145,7 +145,7 @@ server { # default_svid_ttl: The default SVID TTL. Default: 1h. # default_svid_ttl = "1h" - + # omit_x509svid_uid: If true, the subject on X509-SVIDs will not contain # the unique ID attribute. This configurable is deprecated and will be # removed from a future release. @@ -267,6 +267,28 @@ plugins { # } # } + # KeyManager "gcp_kms": A key manager for signing SVIDs which generates + # and stores keys in Google Cloud KMS. + # KeyManager "gcp_kms" { + # plugin_data = { + # # key_metadata_file: A file path location where information about + # # generated keys will be persisted. + # key_metadata_file = "./file_path" + # + # # key_policy_file: A file path location to a custom IAM Policy (v3) + # # in JSON format to be attached to created CryptoKeys. + # # key_policy_file = "custom-gcp-kms-policy.json" + # + # # key_ring: Resource ID of the key ring where the keys managed by this + # # plugin reside. + # # key_ring = "projects/project/locations/location/keyRings/key-ring" + # + # # service_account_file: Path to the service account file used to + # # authenticate with the Google Cloud KMS API. + # # service_account_file = "" + # } + # } + # KeyManager "memory": A key manager for signing SVIDs which only stores # keys in memory and does not actually persist them anywhere. KeyManager "memory" { @@ -329,10 +351,9 @@ plugins { # # app_secret = "" # # } # # } - # } - - # # } - # # } + # # agent_path_template: A URL path portion format of Agent's SPIFFE ID. + # # Describe in text/template format. + # # agent_path_template = "" # } # } @@ -545,7 +566,18 @@ plugins { # # containing configuration to enable interaction with the # # Kubernetes API server. If unset, it is assumed the notifier # # is in-cluster and in-cluster credentials will be used. + # # Required for remote clusters. # # kube_config_file_path = "" + + # # clusters: Extra remote clusters. + # # clusters = [ + # # { + # # namespace = "infra" + # # config_map = "agents" + # # config_map_key = "bootstrap.crt" + # # kube_config_file_path = "/path/to/kubeconfig" + # # } + # # ] # } # } diff --git a/doc/SPIRE101.md b/doc/SPIRE101.md index d0d2ebeea2..2f0bdcc8d0 100644 --- a/doc/SPIRE101.md +++ b/doc/SPIRE101.md @@ -1,10 +1,9 @@ - +# SPIRE ## Overview This walkthrough will guide you through the steps needed to setup a running example of a SPIRE Server and SPIRE Agent. Interaction with the [Workload API](https://github.com/spiffe/go-spiffe/blob/main/v2/proto/spiffe/workload/workload.proto) will be simulated via a command line tool. - ![SPIRE101](images/SPIRE101.png) ## Requirement(s) @@ -13,7 +12,9 @@ This walkthrough will guide you through the steps needed to setup a running exam Clone the SPIRE github repo. - git clone https://github.com/spiffe/spire +```shell +$ git clone https://github.com/spiffe/spire +``` ### Docker Setup @@ -29,148 +30,159 @@ If you don't already have Docker installed, please follow these [installation in | Join Token | Nonce generated by the SPIRE Server to attest SPIRE Agents | | selector | A native property of a node or workload | - - ## Walkthrough -1. Build the development Docker image. +1. Build the development Docker image. - make dev-image + ```shell + $ make dev-image + ``` -2. Run a shell in the development Docker container. +2. Run a shell in the development Docker container. - make dev-shell + ```shell + $ make dev-shell + ``` -3. Create a user with uid 1000. The uid will be registered as a selector of the workload's SPIFFE ID. During kernel based attestation the workload process will be interrogated for the registered uid. +3. Create a user with uid 1000. The uid will be registered as a selector of the workload's SPIFFE ID. During kernel based attestation the workload process will be interrogated for the registered uid. - useradd -u 1000 workload + ```shell + (in dev shell) # useradd -u 1000 workload + ``` -4. Build SPIRE by running the **build** target. The build target builds all the SPIRE binaries. +4. Build SPIRE by running the **build** target. The build target builds all the SPIRE binaries. - make build + ```shell + (in dev shell) # make build + ``` -5. Try running `help` for `entry` sub command. The **spire-server** and **spire-agent** executables have `-—help` option that give details of respective cli options. +5. Try running `help` for `entry` sub command. The **spire-server** and **spire-agent** executables have `-—help` option that give details of respective cli options. - ./bin/spire-server entry --help + ```shell + (in dev shell) # ./bin/spire-server entry --help + ``` -6. View the SPIRE Server configuration file. +6. View the SPIRE Server configuration file. - cat conf/server/server.conf + ```shell + $(in dev shell) # cat conf/server/server.conf + ``` - The default SPIRE Server configurations are shown below. A detailed description of each of the SPIRE Server configuration options is in [the Server documentation](/doc/spire_server.md). + The default SPIRE Server configurations are shown below. A detailed description of each of the SPIRE Server configuration options is in [the Server documentation](/doc/spire_server.md). - ```hcl - server { - bind_address = "127.0.0.1" - bind_port = "8081" - trust_domain = "example.org" - data_dir = "./.data" - log_level = "DEBUG" - default_svid_ttl = "1h" - ca_subject { - country = ["US"] - organization = ["SPIFFE"] - common_name = "" - } - } + ```hcl + server { + bind_address = "127.0.0.1" + bind_port = "8081" + trust_domain = "example.org" + data_dir = "./.data" + log_level = "DEBUG" + } - plugins { - DataStore "sql" { - plugin_data { - database_type = "sqlite3" - connection_string = "./.data/datastore.sqlite3" - } - } + plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "./.data/datastore.sqlite3" + } + } - NodeAttestor "join_token" { - plugin_data { - } - } + NodeAttestor "join_token" { + plugin_data { + } + } - KeyManager "memory" { - plugin_data = {} - } + KeyManager "memory" { + plugin_data = {} + } - UpstreamAuthority "disk" { - plugin_data { - key_file_path = "./conf/server/dummy_upstream_ca.key" - cert_file_path = "./conf/server/dummy_upstream_ca.crt" - } - } - } - ``` + UpstreamAuthority "disk" { + plugin_data { + key_file_path = "./conf/server/dummy_upstream_ca.key" + cert_file_path = "./conf/server/dummy_upstream_ca.crt" + } + } + } + ``` 7. Start the SPIRE Server as a background process by running the following command. - ./bin/spire-server run & + ```shell + (in dev shell) # ./bin/spire-server run & + ``` 8. Generate a one time Join Token via **spire-server token generate** sub command. Use the **-spiffeID** option to associate the Join Token with **spiffe://example.org/host** SPIFFE ID. Save the generated join token in your copy buffer. - ./bin/spire-server token generate -spiffeID spiffe://example.org/host + ```shell + (in dev shell) # ./bin/spire-server token generate -spiffeID spiffe://example.org/host + ``` - The Join Token will be used as a form of node attestation and the associated SPIFFE ID will be assigned to the node. + The Join Token will be used as a form of node attestation and the associated SPIFFE ID will be assigned to the node. - The default ttl of the Join Token is 600 seconds. We can overwrite the default value through **-ttl** option. + The default ttl of the Join Token is 600 seconds. We can overwrite the default value through **-ttl** option. 9. View the configuration file of the SPIRE Agent - cat conf/agent/agent.conf - - The default SPIRE Agent configurations are shown below. A detailed description of each of the SPIRE Agent configuration options is in [the Agent documentation](/doc/spire_agent.md). - ```hcl - agent { - data_dir = "./.data" - log_level = "DEBUG" - server_address = "127.0.0.1" - server_port = "8081" - socket_path ="/tmp/spire-agent/public/api.sock" - trust_bundle_path = "./conf/agent/dummy_root_ca.crt" - trust_domain = "example.org" - } - - plugins { - NodeAttestor "join_token" { - plugin_data { - } - } - KeyManager "disk" { - plugin_data { - directory = "./.data" - } - } - WorkloadAttestor "k8s" { - plugin_data { - kubelet_read_only_port = "10255" - } - } - WorkloadAttestor "unix" { - plugin_data { - } - } - WorkloadAttestor "docker" { - plugin_data { - } - } - } + ```shell + (in dev shell) # cat conf/agent/agent.conf + ``` + + The default SPIRE Agent configurations are shown below. A detailed description of each of the SPIRE Agent configuration options is in [the Agent documentation](/doc/spire_agent.md). + + ```hcl + agent { + data_dir = "./.data" + log_level = "DEBUG" + server_address = "127.0.0.1" + server_port = "8081" + socket_path ="/tmp/spire-agent/public/api.sock" + trust_bundle_path = "./conf/agent/dummy_root_ca.crt" + trust_domain = "example.org" + } + + plugins { + NodeAttestor "join_token" { + plugin_data { + } + } + KeyManager "disk" { + plugin_data { + directory = "./.data" + } + } + WorkloadAttestor "unix" { + plugin_data { + } + } + } + ``` + +10. Start the SPIRE Agent as a background process. Replace `` with the saved value from step #8 in the following command. + + ```shell + (in dev shell) # ./bin/spire-agent run -joinToken & ``` -10. Start the SPIRE Agent as a background process. Replace with the saved value from step #8 in the following command. - - ./bin/spire-agent run -joinToken & - 11. The next step is to register a SPIFFE ID with a set of selectors. For the example we will use unix kernel selectors that will be mapped to a target SPIFFE ID. - ./bin/spire-server entry create \ - -parentID spiffe://example.org/host \ - -spiffeID spiffe://example.org/workload \ - -selector unix:uid:1000 + ```shell + (in dev shell) # ./bin/spire-server entry create \ + -parentID spiffe://example.org/host \ + -spiffeID spiffe://example.org/workload \ + -selector unix:uid:1000 + ``` + At this point, the target workload has been registered with the SPIRE Server. We can now call the Workload API using a command line program to request the workload SVID from the SPIRE Agent. 12. Simulate the Workload API interaction and retrieve the workload SVID bundle by running the `api` subcommand in the agent. Run the command as user **_workload_** created in step #3 with uid 1000 - su -c "./bin/spire-agent api fetch x509 " workload + ```shell + (in dev shell) # su -c "./bin/spire-agent api fetch x509 " workload + ``` 13. Examine the output. Optionally, you may write the SVID and key to disk with `-write` in order to examine them in detail. - su -c "./bin/spire-agent api fetch x509 -write ./" workload - openssl x509 -in ./svid.0.pem -text -noout + ```shell + (in dev shell) # su -c "./bin/spire-agent api fetch x509 -write ./" workload + (in dev shell) # openssl x509 -in ./svid.0.pem -text -noout + ``` diff --git a/doc/auditlog.md b/doc/auditlog.md index 30535b86c0..94a2e74da7 100644 --- a/doc/auditlog.md +++ b/doc/auditlog.md @@ -10,13 +10,14 @@ Each entry contains fields related with the provided request to each endpoint. I |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------|------------------| | type | Constant value that is used to identify that the current entry is an audit log. | audit | | request_id | A uuid that identifies the current call. It is useful for batch operations that can emit multiple audit logs, one per each operation that is done. | | -| status | Indicates if the call was successful or not. | [error, success] | +| status | Indicates if the call was successful or not. | [error, success] | | status_code | In case of an error, contains the gRPC status code. | | | status_message | In case of an error, contains the error returned to the caller. | | The following fields are provided to identify the caller. ### Endpoints listening on UDS + > **_NOTE:_** In order to enable audit log in Kubernetes for calls done on UDS endpoints, `hostPID: true` is required in the SPIRE Server node. | Key | Description | @@ -26,6 +27,7 @@ The following fields are provided to identify the caller. | caller_path | Caller binary file path. | ### Endpoints listening on TLS ports + | Key | Description | |-------------|-------------------------------------------------------------------------------| | caller_addr | Caller IP address. | diff --git a/doc/authorization_policy_engine.md b/doc/authorization_policy_engine.md index 9136bfd8a3..6cf1a9d0b1 100644 --- a/doc/authorization_policy_engine.md +++ b/doc/authorization_policy_engine.md @@ -1,17 +1,17 @@ # Authorization policy engine -**Warning**: Use of custom authorization policies is experiemental and can -result in security degredation if not configured correctly. Please refer to +**Warning**: Use of custom authorization policies is experimental and can +result in security degredation if not configured correctly. Please refer to [this section](#extending-the-policy) for more details on extending the default policy. The authorization decisions in SPIRE are determined by a policy engine which bases its decision on a rego policy and databindings with Open Policy Agent -(OPA). +(OPA). This is a sample configuration of the policy. -``` +```hcl server { experimental { auth_opa_policy_engine { @@ -27,11 +27,11 @@ server { If the policy engine configuration is not set, it defaults to the [default SPIRE authorization policy](#default-configurations). -# Details of the policy engine +## Details of the policy engine The policy engine is based on the [Open Policy Agent (OPA)](https://www.openpolicyagent.org/). This is configured via two -components, the rego policy, and the policy data path (or databindings as +components, the rego policy, and the policy data path (or databindings as referred to in OPA). - The rego policy is a rego policy file defining how to authorize the API calls. @@ -43,12 +43,13 @@ part of the rego and databindings. However, the general rule is "How it is done" is part of the rego policy, and the "What does this apply to" is part of the databindings file. -## Rego policy +### Rego policy The rego policy defines how input to the policy engine is evaluated to produce the result used by SPIRE server for authorization decisions. This is defined by the result object: -``` + +```rego result = { "allow": true/false, "allow_if_admin": true/false, @@ -59,12 +60,13 @@ result = { ``` The fields of the result are the following: + - `allow`: a boolean that if true, will authorize the call - `allow_if_local`: a boolean that if true, will authorize the call only if the caller is a local UNIX socket call - `allow_if_admin`: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID with the Admin flag set -- `allow_if_downstream`: a boolean that if true, will authorize the call +- `allow_if_downstream`: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID that is downstream - `allow_if_agent`: a boolean that is true, will authorize the call only if the caller is an agent. @@ -72,13 +74,14 @@ The fields of the result are the following: The results are evaluated by the following semantics where `isX()` is an evaluation of whether the caller has property `X`. -``` +```rego admit_request = allow || (allow_if_local && isLocal()) || (allow_if_admin && isAdmin()) || (allow_if_downstream && isDownstream()) || (allow_if_agent && isAgent()) ``` The inputs that are passed into the policy are: + - `input`: the input from the SPIRE server for the authorization call - `data`: the databinding from the policy data file @@ -92,7 +95,7 @@ The request (`req`) is the marshalled JSON object from the [SPIRE api sdk](https://github.com/spiffe/spire-api-sdk/). Note that it is not available on client or bidirectional streaming RPC API calls. -## Policy data file (databinding) +### Policy data file (databinding) The policy data file consists of a JSON blob which represents the data that is used in the evaluation of the policy. This is generally free-form and can be @@ -104,7 +107,7 @@ optimized by the policy engine. These data objects can be accessed via the `data` field in the rego policy. For example, a JSON data object may look like this: -``` +```rego { "apis": [ { "full_method": "/spire.api.server.svid.v1.SVID/MintJWTSVID" }, @@ -114,26 +117,26 @@ example, a JSON data object may look like this: } ``` -With the example data object above, we could construct a policy in rego to -check that if the input's full method is equal to one of the objects defined in +With the example data object above, we could construct a policy in rego to +check that if the input's full method is equal to one of the objects defined in the `apis` fields' `full_method` sub-field, then `allow` should be set to true. -``` + +```rego allow = true { input.full_method == data.apis[_].full_method } ``` -### Default configurations +#### Default configurations Here are the default rego policy and policy data values. These are what is required to carry out the default SPIRE authorization decisions. - -#### Default policy.rego +##### Default policy.rego The default rego policy is located [here](/pkg/server/authpolicy/policy.rego). -#### Default policy\_data.json (databindings) +##### Default policy\_data.json (databindings) The default policy\_data.json is located [here](/pkg/server/authpolicy/policy_data.json). @@ -153,37 +156,36 @@ The fields of each object are as follows: | allow_downstream | if true, sets result.allow_if_downstream to true | | | allow_agent | if true, sets result.allow_if_agent to true | | -# Extending the policy +## Extending the policy This section contains examples of how the authorization policy can be extended. -## OPA Warning +### OPA Warning -It is important when implementing custom policies that one understands the +It is important when implementing custom policies that one understands the evaluation semantics and details of OPA rego. An example of subtleties of OPA -rego policy is the evaluation of a variable is taken as a logical OR of all -the clauses. Therefore, creating an additional rule that sets `allow = false` +rego policy is the evaluation of a variable is taken as a logical OR of all +the clauses. Therefore, creating an additional rule that sets `allow = false` will not be an effective addition to the policy. -It is recommended to familiarize yourself with the +It is recommended to familiarize yourself with the [OPA rego language](https://www.openpolicyagent.org/docs/latest/) before implementing custom policies. - -## Example 1a: Entry creation namespacing restrictions +### Example 1a: Entry creation namespacing restrictions In this example, we want to ensure that entries created are namespaced, so we can create namespaces within the trust domain to determine the type of entries that can be created by each client. This would be a scenario of having two departments where one would not be able to create entries for the other. -Note that this example is specifically for calls through the TCP endpoint, where +Note that this example is specifically for calls through the TCP endpoint, where the user corresponds to the SPIFFE ID in the x509 certificate presented during invocation of the API. This can be defined by creating some additional objects in the data binding: -``` +```rego { "entry_create_namespaces": [ { @@ -200,7 +202,8 @@ This can be defined by creating some additional objects in the data binding: The rego policy can then be updated to compare against the dataset of namespaces of users and path prefixes to compare against the entry create input request. -``` + +```rego check_entry_create_namespace { input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry" @@ -217,22 +220,23 @@ The rego policy can then be updated to check for this, an example of an allow clause would look like the following. Note that it is important to check to see how this fits in with the other parts of the rego policy. -``` +```rego # Any allow check allow = true { check_entry_create_namespace } ``` -## Example 1b: Sub-department namespacing with exlcusions +### Example 1b: Sub-department namespacing with exclusions Building on top of the previous example, let's say we want to have sub departments, having schedulers for a subset of paths within the trust domain. This can be done by building on top of the previous example, with the addition -of an exclusion list. +of an exclusion list. In this example, we have two schedulers: -- `schedulers/finance` is able to create paths starting with `/finance` + +- `schedulers/finance` is able to create paths starting with `/finance` - `schedulers/finance/EMEA` is able to create paths starting with `/finance/EMEA` - `schedulers/finance` should not be able to create paths starting with `/finance/EMEA` @@ -240,7 +244,7 @@ In this example, we have two schedulers: To do this, we can use the same policy as the above, adding on an exclusion list. We will use the following policy data: -``` +```rego { "entry_create_namespaces": [ { @@ -259,7 +263,8 @@ list. We will use the following policy data: ``` We can then add a couple lines to check for the exclusion list: -``` + +```rego check_entry_create_namespace { input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry" @@ -284,13 +289,13 @@ check_entry_create_namespace { This will result in the desired boolean outcome to be stored in `check_entry_create_namespace`. -## Example 2: Disallow admin flag in entry creation +### Example 2: Disallow admin flag in entry creation In this second example, we want to restrict it so that we prevent any entries created with an admin flag. This can be done by modifying the rego policy allow clauses with the following check: -``` +```rego check_entry_create_admin_flag { input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry" admin_entries := { entry | entry := input.req.entries[_]; entry.admin == true} @@ -305,23 +310,24 @@ flag. The rego policy can then be updated to check for this, an example of an allow clause would look like the following. Note that it is important to check to see how this fits in with the other parts of the rego policy. -``` + +```rego # Any allow check allow = true { check_entry_create_admin_flag } ``` -## Example 3a: Restrict calls from local UNIX socket +### Example 3a: Restrict calls from local UNIX socket In this example, we want to restrict deletion of entries. For the first part of -this example, we will fully lock down the ability to delete entries. +this example, we will fully lock down the ability to delete entries. This can be easily done by leveraging the set of default rules. In the default policy data file, there are general allow restrictions for APIs. For example, for the batch deletion of entries, here is the exerpt: -``` +```rego { "full_method": "/spire.api.server.entry.v1.Entry/BatchDeleteEntry", "allow_admin": true, @@ -332,13 +338,13 @@ for the batch deletion of entries, here is the exerpt: If we want to disallow deletion of entries from the local or from admin users, we can easily do this by deleting the `allow*` lines, resulting in: -``` +```rego { "full_method": "/spire.api.server.entry.v1.Entry/BatchDeleteEntry", } ``` -## Example 3b: Allow deletion from specific user +### Example 3b: Allow deletion from specific user In this example, we want to now relax our previous restriction by allowing a single SPIFFE ID to perform deletions via the TCP endpoint. @@ -346,7 +352,7 @@ single SPIFFE ID to perform deletions via the TCP endpoint. We can first define the data binding to provide the list of users able to delete entries: -``` +```rego { "entry_delete_users": [ "spiffe://example.org/finance/super-admin-deleter", @@ -355,12 +361,11 @@ entries: } ``` - We can then define the following rego policy to check the calls to the entry delete endpoint, and add checks that the caller SPIFFE ID is in the list of users defined. -``` +```rego check_entry_delete_users { input.full_method == "/spire.api.server.entry.v1.Entry/BatchDeleteEntry" @@ -373,7 +378,7 @@ The rego policy can then be updated to check for this, an example of an allow clause would look like the following. Note that it is important to check to see how this fits in with the other parts of the rego policy. -``` +```rego # Any allow check allow = true { check_entry_delete_users diff --git a/doc/changelog_guidelines.md b/doc/changelog_guidelines.md index 30311ec4ad..359d5f2974 100644 --- a/doc/changelog_guidelines.md +++ b/doc/changelog_guidelines.md @@ -1,6 +1,7 @@ # CHANGELOG Guidelines The following guidelines should be followed when updating the CHANGELOG: + - There should be an entry for every version, that includes the version number and release date. - Entries should be focused on communicating user-facing changes, considering that the main consumers of the CHANGELOG are the end users of SPIRE. - The types of changes should be grouped using the following categories: @@ -18,19 +19,25 @@ The following is an example that includes all the categories: ## [a.b.c] - YYYY-MM-DD ### Added + - AWS PCA now has a configurable allowing operators to provide additional CA certificates for inclusion in the bundle (#1574) ### Changed + - Envoy SDS support is now always on (#1579) ### Deprecated + - The UpstreamCA plugin type is now marked as deprecated in favor of the UpstreamAuthority plugin type (#1406) ### Removed + - The deprecated `upstream_bundle` server configurable has been removed. The server always uses the upstream bundle as the trust bundle (#1702) ### Fixed + - Issue in the Upstream Authority plugin that could result in a delay in the propagation of bundle updates/changes (#1917) ### Security + - Node API now ratelimits expensive calls (#577) diff --git a/doc/plugin_agent_keymanager_disk.md b/doc/plugin_agent_keymanager_disk.md index 3e9bda45c3..04bac75a6e 100644 --- a/doc/plugin_agent_keymanager_disk.md +++ b/doc/plugin_agent_keymanager_disk.md @@ -10,10 +10,10 @@ for long enough for its certificate to expire, attestation will need to be re-pe A sample configuration: -``` - KeyManager "disk" { - plugin_data { - directory = "/opt/spire/data/agent" - } - } +```hcl + KeyManager "disk" { + plugin_data = { + keys_path = "/opt/spire/data/server/keys.json" + } + } ``` diff --git a/doc/plugin_agent_nodeattestor_aws_iid.md b/doc/plugin_agent_nodeattestor_aws_iid.md index 925ef54990..fbb4390df3 100644 --- a/doc/plugin_agent_nodeattestor_aws_iid.md +++ b/doc/plugin_agent_nodeattestor_aws_iid.md @@ -2,13 +2,13 @@ *Must be used in conjunction with the server-side aws_iid plugin* -The `aws_iid` plugin automatically attests instances using the AWS Instance +The `aws_iid` plugin automatically attests instances using the AWS Instance Metadata API and the AWS Instance Identity document. It also allows an operator to use AWS Instance IDs when defining SPIFFE ID attestation policies. Generally no plugin data is needed in AWS, and this configuration should be used: -``` +```hcl NodeAttestor "aws_iid" { plugin_data {} } @@ -18,11 +18,10 @@ Generally no plugin data is needed in AWS, and this configuration should be used |-----------------------|----------------------------------------------------| | ec2_metadata_endpoint | Endpoint for AWS SDK to retrieve instance metadata | - For testing or non-standard AWS environments, you may need to specify the Metadata endpoint. For more information, see [the AWS SDK documentation](https://docs.aws.amazon.com/sdk-for-go/api/aws/ec2metadata/) -``` +```hcl NodeAttestor "aws_iid" { plugin_data { ec2_metadata_endpoint = "http://169.264.169.254/latest" diff --git a/doc/plugin_agent_nodeattestor_azure_msi.md b/doc/plugin_agent_nodeattestor_azure_msi.md index f648485466..c5935bd04a 100644 --- a/doc/plugin_agent_nodeattestor_azure_msi.md +++ b/doc/plugin_agent_nodeattestor_azure_msi.md @@ -2,14 +2,14 @@ *Must be used in conjunction with the server-side azure_msi plugin* -The `azure_msi` plugin attests nodes running in Microsoft Azure that have +The `azure_msi` plugin attests nodes running in Microsoft Azure that have Managed Service Identity (MSI) enabled. Agent nodes acquire a signed MSI token which is passed to the server. The server validates the signed MSI token and extracts the Tenant ID and Principal ID to form the agent SPIFFE ID. The SPIFFE ID has the form: -``` -spiffe:///spire/agent/azure_msi// +```xml +spiffe:///spire/agent/azure_msi// ``` The agent needs to be running in Azure, in a VM with MSI enabled, in order to @@ -17,7 +17,7 @@ use this method of node attestation. | Configuration | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------| -| `resource_id` | The resource ID (or audience) to request for the MSI token. The server will reject tokens with resource IDs it does not recognize | https://management.azure.com/ | +| `resource_id` | The resource ID (or audience) to request for the MSI token. The server will reject tokens with resource IDs it does not recognize | | It is important to note that the resource ID MUST be for a well known Azure service, or an app ID for a registered app in Azure AD. Azure will not issue an @@ -31,7 +31,7 @@ URI that you can use as a resource instead to limit the scope of replay-ability. A sample configuration with the default resource ID (i.e. resource manager): -``` +```hcl NodeAttestor "azure_msi" { plugin_data { } @@ -40,7 +40,7 @@ A sample configuration with the default resource ID (i.e. resource manager): A sample configuration with a custom resource ID: -``` +```hcl NodeAttestor "azure_msi" { plugin_data { resource_id = "http://example.org/app/" diff --git a/doc/plugin_agent_nodeattestor_gcp_iit.md b/doc/plugin_agent_nodeattestor_gcp_iit.md index 43ca53a11f..71f0efdbf3 100644 --- a/doc/plugin_agent_nodeattestor_gcp_iit.md +++ b/doc/plugin_agent_nodeattestor_gcp_iit.md @@ -4,7 +4,6 @@ The `gcp_iit` plugin automatically attests instances using the [GCP Instance Identity Token](https://cloud.google.com/compute/docs/instances/verifying-instance-identity). It also allows an operator to use GCP Instance IDs when defining SPIFFE ID attestation policies. - | Configuration | Description | Default | |---------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------| | identity_token_host | Host where an [identity token](https://cloud.google.com/compute/docs/instances/verifying-instance-identity) can be retrieved from | `metadata.google.internal` | @@ -12,10 +11,11 @@ The `gcp_iit` plugin automatically attests instances using the [GCP Instance Ide A sample configuration: -``` +```hcl NodeAttestor "gcp_iit" { plugin_data { identity_token_host = "metadata.google.internal" service_account = "XXX@developer.gserviceaccount.com" } } +``` diff --git a/doc/plugin_agent_nodeattestor_k8s_psat.md b/doc/plugin_agent_nodeattestor_k8s_psat.md index 91ab12f910..8d5dfccc6a 100644 --- a/doc/plugin_agent_nodeattestor_k8s_psat.md +++ b/doc/plugin_agent_nodeattestor_k8s_psat.md @@ -9,8 +9,8 @@ SPIRE to create more fine-grained attestation policies for agents. The server-side `k8s_psat` plugin will generate a SPIFFE ID on behalf of the agent of the form: -``` -spiffe:///spire/agent/k8s_psat// +```xml +spiffe:///spire/agent/k8s_psat// ``` The main configuration accepts the following values: @@ -20,10 +20,9 @@ The main configuration accepts the following values: | `cluster` | Name of the cluster. It must correspond to a cluster configured in the server plugin. | | | `token_path` | Path to the projected service account token on disk | "/var/run/secrets/tokens/spire-agent" | - A sample configuration with the default token path: -``` +```hcl NodeAttestor "k8s_psat" { plugin_data { cluster = "MyCluster" @@ -32,7 +31,8 @@ A sample configuration with the default token path: ``` Its k8s volume definition: -``` + +```yaml volumes: - name: spire-agent projected: @@ -44,7 +44,8 @@ volumes: ``` And volume mount: -``` + +```yaml volumeMounts: - mountPath: /var/run/secrets/tokens name: spire-agent @@ -52,7 +53,6 @@ volumeMounts: A full example of this attestor is provided in [the SPIRE examples repository](https://github.com/spiffe/spire-examples/tree/main/examples/k8s/simple_psat). - ## Considerations This attestor is based on two Kubernetes beta features (since k8s v1.12): TokenRequest and TokenRequestProjection. TokenRequest exposes the ability to obtain finely scoped service account tokens from the Kubernetes API Server. TokenRequestProjection facilitates the automatic creation and mounting of such a token into a container. diff --git a/doc/plugin_agent_nodeattestor_k8s_sat.md b/doc/plugin_agent_nodeattestor_k8s_sat.md index 3a6270eb6e..1ecbe7b8d4 100644 --- a/doc/plugin_agent_nodeattestor_k8s_sat.md +++ b/doc/plugin_agent_nodeattestor_k8s_sat.md @@ -10,8 +10,8 @@ you should instead consider using the `k8s_psat` attestor due to the [security c The server-side `k8s_sat` plugin generates a one-time UUID and generates a SPIFFE ID with the form: -``` -spiffe:///spire/agent/k8s_sat// +```xml +spiffe:///spire/agent/k8s_sat// ``` The main configuration accepts the following values: @@ -25,7 +25,7 @@ The token path defaults to the default location Kubernetes uses to place the tok A sample configuration with the default token path: -``` +```hcl NodeAttestor "k8s_sat" { plugin_data { cluster = "MyCluster" diff --git a/doc/plugin_agent_nodeattestor_sshpop.md b/doc/plugin_agent_nodeattestor_sshpop.md index 69c4ebf8a8..3cddfc7e13 100644 --- a/doc/plugin_agent_nodeattestor_sshpop.md +++ b/doc/plugin_agent_nodeattestor_sshpop.md @@ -10,8 +10,8 @@ plugin. The SPIFFE ID produced by the server-side `sshpop` plugin is based on the certificate fingerprint, which is an unpadded url-safe base64 encoded sha256 hash of the certificate in openssh format. -``` -spiffe:///spire/agent/sshpop/ +```xml +spiffe:///spire/agent/sshpop/ ``` | Configuration | Description | Default | @@ -21,7 +21,7 @@ spiffe:///spire/agent/sshpop/ A sample configuration: -``` +```hcl NodeAttestor "sshpop" { plugin_data { host_cert_path = "./conf/agent/dummy_agent_ssh_key-cert.pub" diff --git a/doc/plugin_agent_nodeattestor_tpm_devid.md b/doc/plugin_agent_nodeattestor_tpm_devid.md index d592fa7777..90c2a30c07 100644 --- a/doc/plugin_agent_nodeattestor_tpm_devid.md +++ b/doc/plugin_agent_nodeattestor_tpm_devid.md @@ -20,40 +20,40 @@ The proof-of-residency verification involves the creation of a temporary attestation key. Currently, this attestation key is always an RSA key independent of whether the DevID is using an ECC or RSA key type. -The SPIFFE ID produced by the server-side `tpm_devid` plugin is based on the +The SPIFFE ID produced by the server-side `tpm_devid` plugin is based on the LDevID certificate fingerprint, where the fingerprint is defined as the SHA1 hash of the ASN.1 DER encoding of the identity certificate. The SPIFFE ID has the form: -``` -spiffe:///spire/agent/tpm_devid/ +```xml +spiffe:///spire/agent/tpm_devid/ ``` -| Configuration | Description | Default | +| Configuration | Description | Default | |----------------------------------|--------------------------------------------------------------------------------------|-----------------------------------------------------------| -| `tpm_device_path` | The path to a TPM 2.0 device. It is not used when running on windows. | If unset, the plugin will try to autodetect the TPM path | -| `devid_cert_path` | The path to the DevID certificate on disk in PEM format. | | -| `devid_priv_path` | The path to the private key blob generated by the TPM. | | -| `devid_pub_path` | The path to the public key blob generated by the TPM. | | -| `endorsement_hierarchy_password` | TPM endorsement hierarchy password. | "" | -| `owner_hierarchy_password` | TPM owner hierarchy password. | "" | -| `devid_password` | DevID keys password (must be the same than the one used in the provisioning process) | "" | +| `tpm_device_path` | The path to a TPM 2.0 device. It is not used when running on windows. | If unset, the plugin will try to autodetect the TPM path | +| `devid_cert_path` | The path to the DevID certificate on disk in PEM format. | | +| `devid_priv_path` | The path to the private key blob generated by the TPM. | | +| `devid_pub_path` | The path to the public key blob generated by the TPM. | | +| `endorsement_hierarchy_password` | TPM endorsement hierarchy password. | "" | +| `owner_hierarchy_password` | TPM owner hierarchy password. | "" | +| `devid_password` | DevID keys password (must be the same than the one used in the provisioning process) | "" | A sample configuration: -``` - NodeAttestor "tpm_devid" { - plugin_data { - devid_cert_path = "/opt/spire/conf/agent/devid.crt.pem" - devid_priv_path = "/opt/spire/conf/agent/devid.priv.blob" - devid_pub_path = "/opt/spire/conf/agent/devid.pub.blob" - } - } +```hcl + NodeAttestor "tpm_devid" { + plugin_data { + devid_cert_path = "/opt/spire/conf/agent/devid.crt.pem" + devid_priv_path = "/opt/spire/conf/agent/devid.priv.blob" + devid_pub_path = "/opt/spire/conf/agent/devid.pub.blob" + } + } ``` -### Compatibility considerations +## Compatibility considerations + This plugin is designed to work with TPM 2.0, TPM 1.2 is not supported. -+ Only local device identities (LDevIDs) are supported. Attestation using ++ Only local device identities (LDevIDs) are supported. Attestation using IDevIDs is not supported. diff --git a/doc/plugin_agent_nodeattestor_x509pop.md b/doc/plugin_agent_nodeattestor_x509pop.md index 76f343c0e1..3b30d1d856 100644 --- a/doc/plugin_agent_nodeattestor_x509pop.md +++ b/doc/plugin_agent_nodeattestor_x509pop.md @@ -10,8 +10,8 @@ plugin. The SPIFFE ID produced by the server-side `x509pop` plugin is based on the certificate fingerprint, where the fingerprint is defined as the SHA1 hash of the ASN.1 DER encoding of the identity certificate. The SPIFFE ID has the form: -``` -spiffe:///spire/agent/x509pop/ +```xml +spiffe:///spire/agent/x509pop/ ``` | Configuration | Description | Default | @@ -22,11 +22,11 @@ spiffe:///spire/agent/x509pop/ A sample configuration: -``` - NodeAttestor "x509pop" { - plugin_data { - private_key_path = "/opt/spire/conf/agent/agent.key.pem" - certificate_path = "/opt/spire/conf/agent/agent.crt.pem" - } - } +```hcl + NodeAttestor "x509pop" { + plugin_data { + private_key_path = "/opt/spire/conf/agent/agent.key.pem" + certificate_path = "/opt/spire/conf/agent/agent.crt.pem" + } + } ``` diff --git a/doc/plugin_agent_svidstore_aws_secretsmanager.md b/doc/plugin_agent_svidstore_aws_secretsmanager.md index 73927e2d3c..2456f4a87b 100644 --- a/doc/plugin_agent_svidstore_aws_secretsmanager.md +++ b/doc/plugin_agent_svidstore_aws_secretsmanager.md @@ -1,28 +1,28 @@ # Agent plugin: SVIDStore "aws_secretsmanager" -The `aws_secretsmanager` plugin stores in [AWS Secrets Manager](https://aws.amazon.com/es/secrets-manager/) the resulting X509-SVIDs of the entries that the agent is entitled to. +The `aws_secretsmanager` plugin stores in [AWS Secrets Manager](https://aws.amazon.com/es/secrets-manager/) the resulting X509-SVIDs of the entries that the agent is entitled to. -### Secret format +## Secret format The format that is used to store in a secret the issued identity is the following: -``` +```json { - "spiffeId": "spiffe://example.org", - "x509Svid": "X509_CERT_CHAIN_PEM", - "x509SvidKey": "PRIVATE_KET_PEM", - "bundle": "X509_BUNDLE_PEM", - "federatedBundles": { - "spiffe://federated.org": "X509_FEDERATED_BUNDLE_PEM" - } + "spiffeId": "spiffe://example.org", + "x509Svid": "X509_CERT_CHAIN_PEM", + "x509SvidKey": "PRIVATE_KET_PEM", + "bundle": "X509_BUNDLE_PEM", + "federatedBundles": { + "spiffe://federated.org": "X509_FEDERATED_BUNDLE_PEM" + } } ``` -### Required AWS IAM permissions +## Required AWS IAM permissions This plugin requires the following IAM permissions in order to function: -``` +```text secretsmanager:DescribeSecret secretsmanager:CreateSecret secretsmanager:RestoreSecret @@ -34,7 +34,7 @@ kms:Encrypt Please note that this plugin does not read secrets it has stored and therefore does not require read permissions. -### Configuration +## Configuration When the SVIDs are updated, the plugin takes care of updating them in AWS Secrets Manager. @@ -46,7 +46,7 @@ When the SVIDs are updated, the plugin takes care of updating them in AWS Secret A sample configuration: -``` +```hcl SVIDStore "aws_secretsmanager" { plugin_data { access_key_id = "ACCESS_KEY_ID" @@ -56,7 +56,7 @@ A sample configuration: } ``` -### Selectors +## Selectors The selectors of the type `aws_secretsmanager` are used to describe metadata that is needed by the plugin in order to store secret values in AWS Secrets Manager. @@ -65,4 +65,3 @@ The selectors of the type `aws_secretsmanager` are used to describe metadata tha | `aws_secretsmanager:secretname` | `aws_secretsmanager:secretname:some-name` | Friendly name of the secret where the SVID is stored. If not specified `aws_secretsmanager:arn` must be defined | | `aws_secretsmanager:arn` | `aws_secretsmanager:arn:some-arn` | The Amazon Resource Name (ARN) of the secret where the SVID is stored. If not specified, `aws_secretsmanager:secretname` must be defined | | `aws_secretsmanager:kmskeyid` | `aws_secretmanager:kmskeyid` | Specifies the ARN, Key ID, or alias of the AWS KMS customer master key (CMK) to be used to encrypt the secrets. Any of the supported ways to identify a AWS KMS key ID can be used. If a CMK in a different account needs to be referenced, only the key ARN or the alias ARN can be used. If not specified, the AWS account's default CMK is used | - diff --git a/doc/plugin_agent_svidstore_gcp_secretmanager.md b/doc/plugin_agent_svidstore_gcp_secretmanager.md index 7fd0c1f406..90bc5ce015 100644 --- a/doc/plugin_agent_svidstore_gcp_secretmanager.md +++ b/doc/plugin_agent_svidstore_gcp_secretmanager.md @@ -1,44 +1,46 @@ # Agent plugin: SVIDStore "gcp_secretmanager" -The `gcp_secretmanager` plugin stores in [Google cloud Secret Manager](https://cloud.google.com/secret-manager) the resulting X509-SVIDs of the entries that the agent is entitled to. +The `gcp_secretmanager` plugin stores in [Google cloud Secret Manager](https://cloud.google.com/secret-manager) the resulting X509-SVIDs of the entries that the agent is entitled to. -### Secret format +## Secret format The format that is used to store in a secret the issued identity is the following: -``` +```json { - "spiffeId": "spiffe://example.org", - "x509Svid": "X509_CERT_CHAIN_PEM", - "x509SvidKey": "PRIVATE_KET_PEM", - "bundle": "X509_BUNDLE_PEM", - "federatedBundles": { - "spiffe://federated.org": "X509_FEDERATED_BUNDLE_PEM" - } + "spiffeId": "spiffe://example.org", + "x509Svid": "X509_CERT_CHAIN_PEM", + "x509SvidKey": "PRIVATE_KET_PEM", + "bundle": "X509_BUNDLE_PEM", + "federatedBundles": { + "spiffe://federated.org": "X509_FEDERATED_BUNDLE_PEM" + } } ``` -### Required GCP permissions +## Required GCP permissions This plugin requires the following IAM permissions in order to function: -``` + +```text secretmanager.secrets.create secretmanager.secrets.delete secretmanager.secrets.get secretmanager.secrets.update secretmanager.versions.add ``` + Please note that this plugin does not require permission to read secret payloads stored on secret version. -### Configuration +## Configuration -| Configuration | Description | DEFAULT | -|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| -| service_account_file | (Optional) Path to the service account file used to authenticate with the Google Compute Engine API. By default credentails are retrieved from environment. | Value of `GOOGLE_APPLICATION_CREDENTIALS ` environment variable | +| Configuration | Description | DEFAULT | +|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------| +| service_account_file | (Optional) Path to the service account file used to authenticate with the Google Compute Engine API. By default credentials are retrieved from environment. | Value of `GOOGLE_APPLICATION_CREDENTIALS` environment variable | A sample configuration: -``` +```hcl SVIDStore "gcp_secretmanager" { plugin_data { service_account_file = "/opt/token" @@ -46,22 +48,22 @@ A sample configuration: } ``` -### IAM Policy +## IAM Policy It is possible to add an IAM Policy when creating a new secret. This is done using the `role` and `serviceaccount` selectors, which must be configured together. The secret will have the inherited IAM Policy together with the new policy, with a single Binding created. The Binding will use the provided role together with service account as unique member. In case that a role/serviceaccount is not set, the secret will use inherited policies from Secret Manager. -``` +```yaml bindings: - members: - serviceAccount:test-secret@project-id.iam.gserviceaccount.com role: roles/secretmanager.viewer ``` -### Store selectors +## Store selectors -Selectors are used on `storable` entries to describre metadata that is needed by `gcp_secretmanager` in order to store secrets in Google Cloud Secret manager. In case that a `required` selector is not provided, the plugin will return an error at execution time. +Selectors are used on `storable` entries to describe metadata that is needed by `gcp_secretmanager` in order to store secrets in Google Cloud Secret manager. In case that a `required` selector is not provided, the plugin will return an error at execution time. | Selector | Example | Required | Description | |------------------------------------|----------------------------------------------------------------------------------|----------|----------------------------------------------------------------------------| @@ -69,4 +71,3 @@ Selectors are used on `storable` entries to describre metadata that is needed by | `gcp_secretmanager:projectid` | `gcp_secretmanager:projectid:some-project` | x | The Google Cloud project ID which the plugin will use Secret Manager | | `gcp_secretmanager:role` | `gcp_secretmanager:role:roles/secretmanager.viewer` | - | The Google Cloud role id for IAM policy (serviceaccount required when set) | | `gcp_secretmanager:serviceaccount` | `gcp_secretmanager:serviceaccount:test-secret@test-proj.iam.gserviceaccount.com` | - | The Google Cloud Service account for IAM policy (role required when set) | - diff --git a/doc/plugin_agent_workloadattestor_docker.md b/doc/plugin_agent_workloadattestor_docker.md index c5843b8e5d..5086453799 100644 --- a/doc/plugin_agent_workloadattestor_docker.md +++ b/doc/plugin_agent_workloadattestor_docker.md @@ -1,7 +1,7 @@ # Agent plugin: WorkloadAttestor "docker" The `docker` plugin generates selectors based on docker labels for workloads calling the agent. -It does so by retrieving the workload's container ID from its cgroup membership on Unix systems or Job Object names on Windows, +It does so by retrieving the workload's container ID from its cgroup membership on Unix systems or Job Object names on Windows, then querying the docker daemon for the container's labels. | Configuration | Description | Default | @@ -9,18 +9,18 @@ then querying the docker daemon for the container's labels. | docker_socket_path | The location of the docker daemon socket (Unix) | "unix:///var/run/docker.sock" | | docker_version | The API version of the docker daemon. If not specified | | | container_id_cgroup_matchers | A list of patterns used to discover container IDs from cgroup entries (Unix) | -| docker_host | The location of the Docker Engine API endpoint (Windows only) | "npipe:////./pipe/docker_engine" | +| docker_host | The location of the Docker Engine API endpoint (Windows only) | "npipe:////./pipe/docker_engine" | A sample configuration: -``` +```hcl WorkloadAttestor "docker" { plugin_data { } } ``` -### Workload Selectors +## Workload Selectors Since selectors are created dynamically based on the container's docker labels, there isn't a list of known selectors. Instead, each of the container's labels are used in creating the list of selectors. @@ -31,7 +31,7 @@ Instead, each of the container's labels are used in creating the list of selecto | `docker:env` | `docker:env:VAR=val` | The raw string value of each of the container's environment variables. | | `docker:image_id` | `docker:image_id:77af4d6b9913` | The image id of the container. | -### Container ID CGroup Matchers +## Container ID CGroup Matchers The patterns provided should use the wildcard `*` matching token and `` capture token to describe how a container id should be extracted from a cgroup entry. The @@ -39,7 +39,8 @@ given patterns MUST NOT be ambiguous and an error will be returned if multiple patterns can match the same input. Valid Example: -``` + +```hcl container_id_cgroup_matchers = [ "/docker/", "/my.slice/*//*" @@ -47,7 +48,8 @@ Valid Example: ``` Invalid Example: -``` + +```hcl container_id_cgroup_matchers = [ "/a/b/", "/*/b/" @@ -58,18 +60,22 @@ Note: The pattern provided is *not* a regular expression. It is a simplified mat language that enforces a forward slash-delimited schema. ## Example + ### Labels + If a workload container is started with `docker run --label com.example.name=foo [...]`, then workload registration would occur as: -``` -spire-server entry create \ + +```shell +$ spire-server entry create \ -parentID spiffe://example.org/host \ -spiffeID spiffe://example.org/host/foo \ -selector docker:label:com.example.name:foo ``` You can compose multiple labels as selectors. -``` -spire-server entry create \ + +```shell +$ spire-server entry create \ -parentID spiffe://example.org/host \ -spiffeID spiffe://example.org/host/foo \ -selector docker:label:com.example.name:foo @@ -80,8 +86,9 @@ spire-server entry create \ Example of an environment variable selector for the variable `ENVIRONMENT` matching a value of `prod`: -``` -spire-server entry create \ + +```shell +$ spire-server entry create \ -parentID spiffe://example.org/host \ -spiffeID spiffe://example.org/host/foo \ -selector docker:env:ENVIRONMENT=prod diff --git a/doc/plugin_agent_workloadattestor_k8s.md b/doc/plugin_agent_workloadattestor_k8s.md index 02318fa78b..eb6f27a5ce 100644 --- a/doc/plugin_agent_workloadattestor_k8s.md +++ b/doc/plugin_agent_workloadattestor_k8s.md @@ -20,30 +20,36 @@ enabled). In the latter case, the hostname is used to perform certificate server name validation against the kubelet certificate. > **Note** kubelet authentication via bearer token requires that the kubelet be -> started with the `--authentication-token-webhook` flag. +> started with the `--authentication-token-webhook` flag. > See [Kubelet authentication/authorization](https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/) > for details. -> **Note** The kubelet uses the TokenReview API to validate bearer tokens. + + +> **Note** The kubelet uses the TokenReview API to validate bearer tokens. > This requires reachability to the Kubernetes API server. Therefore API server downtime can > interrupt workload attestation. The `--authentication-token-webhook-cache-ttl` kubelet flag > controls how long the kubelet caches TokenReview responses and may help to > mitigate this issue. A large cache ttl value is not recommended however, as > that can impact permission revocation. + + > **Note** Anonymous authentication with the kubelet requires that the > kubelet be started with the `--anonymous-auth` flag. It is discouraged to use anonymous > auth mode in production as it requires authorizing anonymous users to the `nodes/proxy` > resource that maps to some privileged operations, such as executing commands in > containers and reading pod logs. + + **Note** To run on Windows containers, Kubernetes v1.24+ and containerd v1.6+ are required, since [hostprocess](https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/) container is required on the agent container. | Configuration | Description | |--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `disable_container_selectors` | If true, container selectors are not produced. This can be used to produce pod selectors when the workload pod is known but the workload container is not ready at the time of attestation. | -| `kubelet_read_only_port` | The kubelet read-only port. This is mutually exlusive with `kubelet_secure_port`. | +| `kubelet_read_only_port` | The kubelet read-only port. This is mutually exclusive with `kubelet_secure_port`. | | `kubelet_secure_port` | The kubelet secure port. It defaults to `10250` unless `kubelet_read_only_port` is set. | | `kubelet_ca_path` | The path on disk to a file containing CA certificates used to verify the kubelet certificate. Required unless `skip_kubelet_verification` is set. Defaults to the cluster CA bundle `/run/secrets/kubernetes.io/serviceaccount/ca.crt`. | | `skip_kubelet_verification` | If true, kubelet certificate verification is skipped | @@ -53,6 +59,43 @@ since [hostprocess](https://kubernetes.io/docs/tasks/configure-pod-container/cre | `use_anonymous_authentication` | If true, use anonymous authentication for kubelet communication | | `node_name_env` | The environment variable used to obtain the node name. Defaults to `MY_NODE_NAME`. | | `node_name` | The name of the node. Overrides the value obtained by the environment variable specified by `node_name_env`. | +| `experimental` | The experimental options that are subject to change or removal. | + +| Experimental options | Description | +|----------------------|----------------------------------------------------------------------------------------------------------------------------- | +| `sigstore` | Sigstore options. Options described below. See [Sigstore workload attestor for SPIRE](#sigstore-workload-attestor-for-spire) | + +| Sigstore options | Description | +|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `skip_signature_verification_image_list` | The list of images, described as digest hashes, that should be skipped in signature verification. Defaults to empty list. | +| `allowed_subjects_list` | A map of allowed subject strings, keyed by the OIDC Provider URI, that are trusted and are allowed to sign container images artifacts. Defaults to empty. If empty, no workload will pass signature validation, unless listed on `skip_signature_verification_image_list`. (eg. `"https://accounts.google.com" = ["subject1@example.com","subject2@example.com"]`). | +| `rekor_url` | The rekor URL to use with cosign. Required. See notes below. | +| `enforce_sct` | A boolean to be set to false in case of a private deployment, not using public CT | + +> **Note** Cosign discourages the use of image tags for referencing docker images, and this plugin does not support attestation of sigstore selectors for workloads running on containers using tag-referenced images, which will then fail attestation for both sigstore and k8s selectors. In cases where this is necessary, add the digest string for the image in the `skip_signature_verification_image_list` setting (eg. `"sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"`). Note that sigstore signature attestation will still not be performed, but this will allow k8s selectors to be returned, along with the `"k8s:sigstore-validation:passed"` selector. + + + +> **Note** Since the SPIRE Agent can also go through workload attestation, it will also need to be included in the skip list if either its image is not signed or has a digest reference string. + + + +> **Note** The sigstore project contains a transparency log called Rekor that provides an immutable, tamper-resistant ledger to record signed metadata to an immutable record. While it is possible to run your own instance, a public instance of rekor is available at `https://rekor.sigstore.dev/`. + +## Sigstore workload attestor for SPIRE + +### Platform support + +This capability is only supported on Unix systems. + +The k8s workload attestor plugin also has capabilities to validate container images signatures through [sigstore](https://www.sigstore.dev/) + +Cosign supports container signing, verification, and storage in an OCI registry. Cosign aims to make signatures invisible infrastructure. For this, we’ve chosen the Sigstore ecosystem and artifacts. Digging deeper, we are using: Rekor (signature transparency log), Fulcio (signing certificate issuer and certificate transparency log) and Cosign (container image signing tool) to guarantee the authenticity of the running workload. + +> **Note** you can provide your own CA roots signed through TUF via the cosign initialize command. +This effectively securely pins the CA roots. We allow you to also specify trusted roots via the `SIGSTORE_ROOT_FILE` flag + +### K8s selectors | Selector | Value | |--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -71,15 +114,24 @@ since [hostprocess](https://kubernetes.io/docs/tasks/configure-pod-container/cre | k8s:pod-init-image | An Image OR ImageID of any init container in the workload's pod, [as reported by K8S](https://pkg.go.dev/k8s.io/api/core/v1#ContainerStatus). Selector value may be an image tag, such as: `docker.io/envoyproxy/envoy-alpine:v1.16.0`, or a resolved SHA256 image digest, such as `docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb` | | k8s:pod-init-image-count | The number of init container images in workload's pod | -> **Note** `container-image` will ONLY match against the specific container in the pod that is contacting SPIRE on behalf of -> the pod, whereas `pod-image` and `pod-init-image` will match against ANY container or init container in the Pod, +Sigstore enabled selectors (available when configured to use sigstore) + +| Selector | Value | +|----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| k8s:${containerID}:image-signature-content | A containerID is an unique alphanumeric number for each container. The value of the signature itself in a hash (eg. "k8s:000000:image-signature-content:MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=") | +| k8s:${containerID}:image-signature-subject | OIDC principal that signed it​ (eg. "k8s:000000:image-signature-subject:spirex@example.com") | +| k8s:${containerID}:image-signature-logid | A unique LogID for the Rekor transparency log​ (eg. "k8s:000000:image-signature-logid:samplelogID") | +| k8s:${containerID}:image-signature-integrated-time | The time (in Unix timestamp format) when the image signature was integrated into the signature transparency log​ (eg. "k8s:000000:image-signature-integrated-time:12345") | +| k8s:sigstore-validation | The confirmation if the signature is valid, has value of "passed" (eg. "k8s:sigstore-validation:passed") | +> **Note** `container-image` will ONLY match against the specific container in the pod that is contacting SPIRE on behalf of +> the pod, whereas `pod-image` and `pod-init-image` will match against ANY container or init container in the Pod, > respectively. ## Examples To use the kubelet read-only port: -``` +```hcl WorkloadAttestor "k8s" { plugin_data { kubelet_read_only_port = 10255 @@ -89,7 +141,7 @@ WorkloadAttestor "k8s" { To use the secure kubelet port, verify via `/run/secrets/kubernetes.io/serviceaccount/ca.crt`, and authenticate via the default service account token: -``` +```hcl WorkloadAttestor "k8s" { plugin_data { } @@ -98,7 +150,7 @@ WorkloadAttestor "k8s" { To use the secure kubelet port, skip verification, and authenticate via the default service account token: -``` +```hcl WorkloadAttestor "k8s" { plugin_data { skip_kubelet_verification = true @@ -108,7 +160,7 @@ WorkloadAttestor "k8s" { To use the secure kubelet port, skip verification, and authenticate via some other token: -``` +```hcl WorkloadAttestor "k8s" { plugin_data { skip_kubelet_verification = true @@ -119,7 +171,7 @@ WorkloadAttestor "k8s" { To use the secure kubelet port, verify the kubelet certificate, and authenticate via an X509 client certificate: -``` +```hcl WorkloadAttestor "k8s" { plugin_data { kubelet_ca_path = "/path/to/kubelet-ca.pem" diff --git a/doc/plugin_agent_workloadattestor_unix.md b/doc/plugin_agent_workloadattestor_unix.md index 8ef8decffe..57b99adeb8 100644 --- a/doc/plugin_agent_workloadattestor_unix.md +++ b/doc/plugin_agent_workloadattestor_unix.md @@ -50,13 +50,11 @@ Defenses against this are: A sample configuration: -``` - WorkloadAttestor "unix" { - plugin_data { - } - } +```hcl + WorkloadAttestor "unix" { + } ``` -### Platform support +## Platform support This plugin is only supported on Unix systems. diff --git a/doc/plugin_agent_workloadattestor_windows.md b/doc/plugin_agent_workloadattestor_windows.md index 23fb70b242..5cd8c85228 100644 --- a/doc/plugin_agent_workloadattestor_windows.md +++ b/doc/plugin_agent_workloadattestor_windows.md @@ -8,7 +8,7 @@ It does so by opening an access token associated with the workload process. The | `discover_workload_path` | If true, the workload path will be discovered by the plugin and used to provide additional selectors | false | | `workload_size_limit` | The limit of workload binary sizes when calculating certain selectors (e.g. sha256). If zero, no limit is enforced. If negative, never calculate the hash. | 0 | -### Workload Selectors +## Workload Selectors | Selector | Value | |---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -39,20 +39,21 @@ Defenses against this are: The workload API does not yet support rate limiting, but when it does, this attack can be mitigated by using rate limiting in conjunction with non-negative `workload_size_limit`. -#### Notes +### Notes + - An enabled group in a token is a group that has the [SE_GROUP_ENABLED](https://docs.microsoft.com/en-us/windows/win32/secauthz/sid-attributes-in-an-access-token) attribute. - User and group account names are expressed using the [down-level logon name format](https://docs.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#down-level-logon-name). -### Configuration +## Configuration This plugin does not require any configuration setting. It can be added in the following way in the agent configuration file: -``` - WorkloadAttestor "windows" { - } +```hcl + WorkloadAttestor "windows" { + } ``` -### Platform support +## Platform support This plugin is only supported on Windows. diff --git a/doc/plugin_server_datastore_sql.md b/doc/plugin_server_datastore_sql.md index ba821b918a..a4430d7b0f 100644 --- a/doc/plugin_server_datastore_sql.md +++ b/doc/plugin_server_datastore_sql.md @@ -15,8 +15,6 @@ The `sql` plugin implements SQL based data storage for the SPIRE server using SQ | conn_max_lifetime | The maximum amount of time a connection may be reused (default: unlimited) | | disable_migration | True to disable auto-migration functionality. Use of this flag allows finer control over when datastore migrations occur and coordination of the migration of a datastore shared with a SPIRE Server cluster. Only available for databases from SPIRE Code version 0.9.0 or later. | - - For more information on the `max_open_conns`, `max_idle_conns`, and `conn_max_lifetime`, refer to the documentation for the Go [`database/sql`](https://golang.org/pkg/database/sql/#DB) package. @@ -25,12 +23,14 @@ documentation for the Go [`database/sql`](https://golang.org/pkg/database/sql/#D ### `database_type = "sqlite3"` Save database in file: -``` + +```hcl connection_string="DATABASE_FILE.db" ``` Save database in memory: -``` + +```hcl connection_string="file:memdb?mode=memory&cache=shared" ``` @@ -38,7 +38,7 @@ If you are compiling SPIRE from source, please see [SQLite and CGO](#sqlite-and- #### Sample configuration -``` +```hcl DataStore "sql" { plugin_data { database_type = "sqlite3" @@ -53,11 +53,12 @@ The `connection_string` for the PostreSQL database connection consists of the nu For example: -``` +```hcl connection_string="dbname=postgres user=postgres password=password host=localhost sslmode=disable" ``` #### Configuration Options + * dbname - The name of the database to connect to * user - The user to sign in as * password - The user's password @@ -75,6 +76,7 @@ connection_string="dbname=postgres user=postgres password=password host=localhos must contain PEM encoded data. #### Valid sslmode configurations + * disable - No SSL * require - Always SSL (skip verification) * verify-ca - Always SSL (verify that the certificate presented by the @@ -85,7 +87,7 @@ connection_string="dbname=postgres user=postgres password=password host=localhos #### Sample configuration -``` +```hcl DataStore "sql" { plugin_data { database_type = "postgres" @@ -98,19 +100,20 @@ connection_string="dbname=postgres user=postgres password=password host=localhos The `connection_string` for the MySQL database connection consists of the number of configuration options (optional parts marked by square brackets): -```` +```text username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] -```` +``` For example: -``` +```hcl connection_string="username:password@tcp(localhost:3306)/dbname?parseTime=true" ``` Consult the [MySQL driver repository](https://github.com/go-sql-driver/mysql#usage) for more `connection_string` options. #### Configuration Options + * dbname - The name of the database to connect to * username - The user to sign in as * password - The user's password @@ -121,7 +124,7 @@ If you need to use custom Root CA, just specify `root_ca_path` in the plugin con #### Sample configuration -``` +```hcl DataStore "sql" { plugin_data { database_type = "mysql" @@ -131,8 +134,9 @@ If you need to use custom Root CA, just specify `root_ca_path` in the plugin con ``` #### Read Only connection + Read Only connection will be used when the optional `ro_connection_string` is set. The formatted string takes the same form as connection_string. This option is not applicable for SQLite3. ## SQLite and CGO -SQLite support requires the use of CGO. This is not a concern for users downloading SPIRE or using the offical SPIRE container images. However, if you are building SPIRE from the source code, please note that compiling SPIRE without CGO (e.g. `CGO_ENABLED=0`) will disable SQLite support. +SQLite support requires the use of CGO. This is not a concern for users downloading SPIRE or using the official SPIRE container images. However, if you are building SPIRE from the source code, please note that compiling SPIRE without CGO (e.g. `CGO_ENABLED=0`) will disable SQLite support. diff --git a/doc/plugin_server_keymanager_aws_kms.md b/doc/plugin_server_keymanager_aws_kms.md index 669f98e9b4..d76086e432 100644 --- a/doc/plugin_server_keymanager_aws_kms.md +++ b/doc/plugin_server_keymanager_aws_kms.md @@ -14,7 +14,6 @@ The plugin accepts the following configuration options: | key_metadata_file | string | yes | A file path location where information about generated keys will be persisted | | | key_policy_file | string | no | A file path location to a custom key policy in JSON format | "" | - ### Alias and Key Management The plugin assigns [aliases](https://docs.aws.amazon.com/kms/latest/developerguide/kms-alias.html) to the Customer Master Keys that manages. The aliases are used to identify and name keys that are managed by the plugin. @@ -44,55 +43,56 @@ The IAM role must have an attached policy with the following permissions: - `kms:UpdateAlias` - `kms:DeleteAlias` - ### Key policy + The plugin can generate keys using a default key policy or it can load and use a user defined policy. #### Default key policy + The default key policy relies on the SPIRE Server's assumed role. Therefore, it is mandatory for SPIRE server to assume a role in order to use the default policy. ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "Allow full access to the SPIRE Server role", - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::111122223333:role/example-assumed-role-name" - }, - "Action": "kms:*", - "Resource": "*" - }, + "Version": "2012-10-17", + "Statement": [ { - "Sid": "Allow KMS console to display the key and policy", + "Sid": "Allow full access to the SPIRE Server role", "Effect": "Allow", "Principal": { - "AWS": "arn:aws:iam::111122223333:root" + "AWS": "arn:aws:iam::111122223333:role/example-assumed-role-name" }, - "Action": [ - "kms:Describe*", - "kms:List*", - "kms:Get*" - ], + "Action": "kms:*", "Resource": "*" - } - ] + }, + { + "Sid": "Allow KMS console to display the key and policy", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::111122223333:root" + }, + "Action": [ + "kms:Describe*", + "kms:List*", + "kms:Get*" + ], + "Resource": "*" + } + ] } ``` - The first statement of the policy gives the current SPIRE server assumed role full access to the CMK. - The second statement allows the keys and policy to be displayed in the KMS console. - #### Custom key policy It is also possible for the user to define a custom key policy. If the configurable `key_policy_file` is set, the plugin uses the policy defined in the file instead of the default policy. + ## Sample Plugin Configuration -``` +```hcl KeyManager "aws_kms" { plugin_data { region = "us-east-2" diff --git a/doc/plugin_server_keymanager_disk.md b/doc/plugin_server_keymanager_disk.md index 25bd6890d4..b5b3e67026 100644 --- a/doc/plugin_server_keymanager_disk.md +++ b/doc/plugin_server_keymanager_disk.md @@ -11,10 +11,10 @@ The plugin accepts the following configuration options: A sample configuration: -``` - KeyManager "disk" { - plugin_data = { - keys_path = "/opt/spire/data/server/keys.json" - } - } +```hcl + KeyManager "disk" { + plugin_data = { + keys_path = "/opt/spire/data/server/keys.json" + } + } ``` diff --git a/doc/plugin_server_keymanager_gcp_kms.md b/doc/plugin_server_keymanager_gcp_kms.md new file mode 100644 index 0000000000..da2e9fa673 --- /dev/null +++ b/doc/plugin_server_keymanager_gcp_kms.md @@ -0,0 +1,158 @@ +# Server plugin: KeyManager "gcp_kms" + +The `gcp_kms` key manager plugin leverages the Google Cloud Key Management +Service to create, maintain, and rotate key pairs, signing SVIDs as needed. No +Google Cloud principal can view or export the raw cryptographic key material +represented by a key. Instead, Cloud KMS accesses the key material on behalf of +SPIRE. + +## Configuration + +The plugin accepts the following configuration options: + +| Key | Type | Required | Description | Default | +| --- | ---- | -------- | ----------- | ------- | +| key_policy_file | string | no | A file path location to a custom [IAM Policy (v3)](https://cloud.google.com/pubsub/docs/reference/rpc/google.iam.v1#google.iam.v1.Policy) in JSON format to be attached to created CryptoKeys. | "" | +| key_metadata_file | string | yes | A file path location where key metadata used by the plugin will be persisted. See "[Management of keys](#management-of-keys)" for more information. | "" | +| key_ring | string | yes | Resource ID of the key ring where the keys managed by this plugin reside, in the format projects/\*/locations/\*/keyRings/\* | "" | +| service_account_file | string | no | Path to the service account file used to authenticate with the Cloud KMS API. | Value of `GOOGLE_APPLICATION_CREDENTIALS` environment variable. | + +### Authenticating with the Cloud KMS API + +The plugin uses the Application Default Credentials to authenticate with the +Google Cloud KMS API, as documented by [Setting Up Authentication For Server to +Server](https://cloud.google.com/docs/authentication/production). When SPIRE +Server is running inside GCP, it will use the default service account +credentials available to the instance it is running under. When running outside +GCP, or if non-default credentials are needed, the path to the service account +file containing the credentials may be specified using the +`GOOGLE_APPLICATION_CREDENTIALS` environment variable or the +`service_account_file` configurable (see [Configuration](#configuration)). + +### Use of key versions + +In Cloud KMS, the cryptographic key material that is used to sign data is stored +in a key version (CryptoKeyVersion). A key (CryptoKey) can have zero or more key +versions. + +For each SPIRE Key ID that the server manages, this plugin maintains a +CryptoKey. When a key is rotated, a new CryptoKeyVersion is added to the +CryptoKey and the rotated CryptoKeyVersion is scheduled for destruction. + +### Management of keys + +The plugin assigns +[labels](https://cloud.google.com/kms/docs/creating-managing-labels) to the +CryptoKeys that it manages in order to keep track of them. The use of these +labels also allows efficient filtering when performing the listing operations in +the service. All the labels are named with the `spire-` prefix. +Users don't need to interact with the labels managed by the plugin. The +following table is provided for informational purposes only: + +| Label | Description | +| ----- | ----------- | +| spire-server-td | SHA-1 checksum of the trust domain name of the server. | +| spire-server-id | Auto-generated ID that is unique to the server and is persisted in the _Key Metadata File_ (see the `key_metadata_file` configurable). | +| spire-last-update | Unix time of the last time that the plugin updated the CryptoKey to keep it active. | +| spire-active | Indicates if the CryptoKey is still in use by the plugin. | + +If the _Key Metadata File_ is not found during server startup, the file is +recreated, with a new auto-generated server ID. Consequently, if the file is +lost, the plugin will not be able to identify keys that it has previously +managed and will recreate new keys on demand. + +The plugin attempts to detect and prune stale CryptoKeys. To facilitate stale +CryptoKey detection, the plugin actively updates the `spire-last-update` label +on all CryptoKeys managed by the server every 6 hours. The plugin periodically +scans the CryptoKeys looking for active CryptoKeys within the trust domain that +have a `spire-last-update` value older than two weeks and don't belong to the +server. The corresponding CryptoKeyVersions of those stale CryptoKeys are +scheduled for destruction, and the `spire-active` label in the CryptoKey is +updated to indicate that the CryptoKey is no longer active. Additionally, if +the plugin detects that a CryptoKey doesn't have any enabled CryptoKeyVersions, +it also updates the `spire-active` label in the CryptoKey to set it as inactive. + +### Required permissions + +The plugin requires the following IAM permissions be granted to the +authenticated service account in the configured key ring: + +```text +cloudkms.cryptoKeys.create +cloudkms.cryptoKeys.getIamPolicy +cloudkms.cryptoKeys.list +cloudkms.cryptoKeys.setIamPolicy +cloudkms.cryptoKeys.update +cloudkms.cryptoKeyVersions.create +cloudkms.cryptoKeyVersions.destroy +cloudkms.cryptoKeyVersions.get +cloudkms.cryptoKeyVersions.list +cloudkms.cryptoKeyVersions.useToSign +cloudkms.cryptoKeyVersions.viewPublicKey +``` + +### IAM policy + +Google Cloud resources are organized hierarchically, and resources inherit the +allow policies of the parent resource. The plugin sets a default IAM policy to +CryptoKeys that it creates. Alternatively, a user defined IAM policy can be +defined. +The effective allow policy for a CryptoKey is the union of the allow policy set +at that resource by the plugin and the allow policy inherited from its parent. + +#### Default IAM policy + +The plugin defines a default IAM policy that is set to created CryptoKeys. This +policy binds the authenticated service account with the Cloud KMS CryptoKey +Signer/Verifier (`roles/cloudkms.signerVerifier`) predefined role. + +```json +{ + "bindings": [ + { + "role": "roles/cloudkms.signerVerifier", + "members": [ + "serviceAccount:SERVICE_ACCOUNT_EMAIL" + ] + } + ], + "version": 3 +} + +``` + +The `roles/cloudkms.signerVerifier` role grants the following permissions: + +```text +cloudkms.cryptoKeyVersions.useToSign +cloudkms.cryptoKeyVersions.useToVerify +cloudkms.cryptoKeyVersions.viewPublicKey +cloudkms.locations.get +cloudkms.locations.list +resourcemanager.projects.get +``` + +#### Custom IAM policy + +It is also possible for the user to define a custom IAM policy that will be +attached to the created CryptoKeys. If the configurable `key_policy_file` is +set, the plugin uses the policy defined in the file instead of the default +policy. +Custom IAM policies must be defined using +[version 3](https://cloud.google.com/iam/docs/policies#versions). + +## Sample Plugin Configuration + +```hcl +KeyManager "gcp_kms" { + plugin_data { + key_ring = "projects/project-id/locations/location/keyRings/keyring" + key_metadata_file = "./gcpkms-key-metadata" + } +} +``` + +## Supported Key Types + +The plugin supports all the key types supported by SPIRE: `rsa-2048`, +`rsa-4096`, `ec-p256`, and `ec-p384`. diff --git a/doc/plugin_server_nodeattestor_aws_iid.md b/doc/plugin_server_nodeattestor_aws_iid.md index 1b7019187c..9e50377601 100644 --- a/doc/plugin_server_nodeattestor_aws_iid.md +++ b/doc/plugin_server_nodeattestor_aws_iid.md @@ -1,4 +1,5 @@ # Server plugin: NodeAttestor "aws_iid" + *Must be used in conjunction with the agent-side aws_iid plugin* The `aws_iid` plugin automatically attests instances using the AWS Instance @@ -9,6 +10,7 @@ attested by the aws_iid attestor will be issued a SPIFFE ID like this plugin resolves the agent's AWS IID-based SPIFFE ID into a set of selectors. ## Configuration + | Configuration | Description | Default | |--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------| | `access_key_id` | AWS access key id | Value of `AWS_ACCESS_KEY_ID` environment variable | @@ -19,11 +21,11 @@ this plugin resolves the agent's AWS IID-based SPIFFE ID into a set of selectors A sample configuration: -``` +```hcl NodeAttestor "aws_iid" { plugin_data { - access_key_id = "ACCESS_KEY_ID" - secret_access_key = "SECRET_ACCESS_KEY" + access_key_id = "ACCESS_KEY_ID" + secret_access_key = "SECRET_ACCESS_KEY" } } ``` @@ -31,21 +33,25 @@ A sample configuration: If `assume_role` is set, the spire server will assume the role as specified by the template `arn:aws:iam::{{AccountID}}:role/{{AssumeRole}}` where `AccountID` is taken from the AWS IID document sent by the spire agent to the spire server and `AssumeRole` comes from the AWS NodeAttestor plugin configuration. In the following configuration, -``` + +```hcl NodeAttestor "aws_iid" { plugin_data { assume_role = "spire-server-delegate" } } ``` + assuming AWS IID document sent from the spire agent contains `accountId : 12345678`, the spire server will assume "arn:aws:iam::12345678:role/spire-server-delegate" role before making any AWS call for the node attestation. If `assume_role` is configured, the spire server will always assume the role even if the both the spire-server and the spire agent is deployed in the same account. ## Disabling Instance Profile Selectors + In cases where spire-server is running in a location with no public internet access available, setting `disable_instance_profile_selectors = true` will prevent the server from making requests to `iam.amazonaws.com`. This is needed as spire-server will fail to attest nodes as it cannot retrieve the metadata information. When this is enabled, `IAM Role` selector information will no longer be available for use. ## AWS IAM Permissions + The user or role identified by the configured credentials must have permissions for `ec2:DescribeInstances`. The following is an example for a IAM policy needed to get instance's info from AWS. @@ -67,23 +73,29 @@ The following is an example for a IAM policy needed to get instance's info from } ``` -For more information on security credentials, see https://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html. +For more information on security credentials, see . ## Supported Selectors + This plugin generates the following selectors related to the instance where the agent is running: | Selector | Example | Description | |---------------------|-------------------------------------------------------|------------------------------------------------------------------| +| Availability Zone | `aws_iid:az:us-west-2b` | The Availability Zone in which the instance is running. | +| IAM role | `aws_iid:iamrole:arn:aws:iam::123456789012:role/Blog` | An IAM role within the instance profile for the instance | +| Image ID | `aws_iid:image:id:ami-5fb8c835` | The ID of the AMI used to launch the instance. | +| Instance ID | `aws_iid:instance:id:i-0b22a22eec53b9321` | The ID of the instance. | | Instance Tag | `aws_iid:tag:name:blog` | The key (e.g. `name`) and value (e.g. `blog`) of an instance tag | +| Region | `aws_iid:region:us-west-2` | The Region in which the instance is running. | | Security Group ID | `aws_iid:sg:id:sg-01234567` | The id of the security group the instance belongs to | | Security Group Name | `aws_iid:sg:name:blog` | The name of the security group the instance belongs to | -| IAM role | `aws_iid:iamrole:arn:aws:iam::123456789012:role/Blog` | An IAM role within the instance profile for the instance | All of the selectors have the type `aws_iid`. The `IAM role` selector is included in the generated set of selectors only if the instance has an IAM Instance Profile associated and `disable_instance_profile_selectors = false` ## Security Considerations + The AWS Instance Identity Document, which this attestor leverages to prove node identity, is available to any process running on the node by default. As a result, it is possible for non-agent code running on a node to attest to the SPIRE Server, allowing it to obtain any workload identity that the node is authorized to run. While many operators choose to configure their systems to block access to the Instance Identity Document, the SPIRE project cannot guarantee this posture. To mitigate the associated risk, the `aws_iid` node attestor implements Trust On First Use (or TOFU) semantics. For any given node, attestation may occur only once. Subsequent attestation attempts will be rejected. diff --git a/doc/plugin_server_nodeattestor_azure_msi.md b/doc/plugin_server_nodeattestor_azure_msi.md index 90a10785f3..c64b50bdf9 100644 --- a/doc/plugin_server_nodeattestor_azure_msi.md +++ b/doc/plugin_server_nodeattestor_azure_msi.md @@ -8,29 +8,29 @@ which is passed to the server. The server validates the signed MSI token and extracts the Tenant ID and Principal ID to form the agent SPIFFE ID. The SPIFFE ID has the form: -``` -spiffe:///spire/agent/azure_msi// +```xml +spiffe:///spire/agent/azure_msi// ``` The server does not need to be running in Azure in order to perform node -attestation or to resolve selectors. +attestation or to resolve selectors. ## Configuration -| Configuration | Required | Description | Default | -| --------------- | ----------- | ----------------------- | -| `tenants` | Required | A map of tenants, keyed by tenant ID, that are authorized for attestation. Tokens for unspecified tenants are rejected. | | - +| Configuration | Required | Description | Default | +|-----------------------|----------|-------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------| +| `tenants` | Required | A map of tenants, keyed by tenant ID, that are authorized for attestation. Tokens for unspecified tenants are rejected. | | +| `agent_path_template` | Optional | A URL path portion format of Agent's SPIFFE ID. Describe in text/template format. | `"/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}"` | Each tenant in the main configuration supports the following -| Configuration | Required | Description | Default | -| ----------------- | ----------- | ----------------------- | -| `resource_id` | Optional | The resource ID (or audience) for the tenant's MSI token. Tokens for a different resource ID are rejected | https://management.azure.com/ | -| `use_msi` | [Optional](#authenticating-to-azure) | Whether or not to use MSI to authenticate to Azure services for selector resolution. | false | -| `subscription_id` | [Optional](#authenticating-to-azure) | The subscription the tenant resides in | | -| `app_id` | [Optional](#authenticating-to-azure) | The application id | | -| `app_secret` | [Optional](#authenticating-to-azure) | The application secret | | +| Configuration | Required | Description | Default | +|-------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------|---------------------------------| +| `resource_id` | Optional | The resource ID (or audience) for the tenant's MSI token. Tokens for a different resource ID are rejected | | +| `use_msi` | [Optional](#authenticating-to-azure) | Whether or not to use MSI to authenticate to Azure services for selector resolution. | false | +| `subscription_id` | [Optional](#authenticating-to-azure) | The subscription the tenant resides in | | +| `app_id` | [Optional](#authenticating-to-azure) | The application id | | +| `app_secret` | [Optional](#authenticating-to-azure) | The application secret | | It is important to note that the resource ID MUST be for a well known Azure service, or an app ID for a registered app in Azure AD. Azure will not issue an @@ -46,14 +46,14 @@ Each tenant can be configured to either authenticate with an MSI token (`subscription_id`, `app_id`, and `app_secret`). The SPIRE Server must reside in the same tenant when authenticating with an MSI token. -For backwards compatability reasons the authentication configuration is *NOT* +For backwards compatibility reasons the authentication configuration is *NOT* required, however, it will be in a future release. ### Sample Configurations #### Default Resource ID and App Authentication -``` +```hcl NodeAttestor "azure_msi" { plugin_data { tenants = { @@ -68,9 +68,9 @@ required, however, it will be in a future release. } ``` -#### Custom Reseource ID and MSI Authentication +#### Custom Resource ID and MSI Authentication -``` +```hcl NodeAttestor "azure_msi" { plugin_data { tenants = { @@ -93,11 +93,25 @@ The plugin produces the following selectors. | Virtual Machine Name | `vm-name:frontend:blog` | The name of the virtual machine (e.g. `blog`) qualified by the resource group (e.g. `frontend`) | | Network Security Group | `network-security-group:frontend:webservers` | The name of the network security group (e.g. `webservers`) qualified by the resource group (e.g. `frontend`) | | Virtual Network | `virtual-network:frontend:vnet` | The name of the virtual network (e.g. `vnet`) qualified by the resource group (e.g. `frontend`) | -| Virtual Network Subnet | `virtual-network:frontend:vnet:default` | The name of the virtual network subnet (e.g. `default`) qualfied by the virtual network and resource group | +| Virtual Network Subnet | `virtual-network:frontend:vnet:default` | The name of the virtual network subnet (e.g. `default`) qualified by the virtual network and resource group | All of the selectors have the type `azure_msi`. +## Agent Path Template + +The agent path template is a way of customizing the format of generated SPIFFE IDs for agents. +The template formatter is using Golang text/template conventions, it can reference values provided by the plugin or in a [MSI access token](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#payload-claims). + +Some useful values are: + +| Value | Description | +|-----------------------|------------------------------------------------------------| +| .PluginName | The name of the plugin | +| .TenantID | Azure tenant identifier | +| .PrincipalID | A identifier that is unique to a particular application ID | + ## Security Considerations + The Azure Managed Service Identity token, which this attestor leverages to prove node identity, is available to any process running on the node by default. As a result, it is possible for non-agent code running on a node to attest to the SPIRE Server, allowing it to obtain any workload identity that the node is authorized to run. While many operators choose to configure their systems to block access to the Managed Service Identity token, the SPIRE project cannot guarantee this posture. To mitigate the associated risk, the `azure_msi` node attestor implements Trust On First Use (or TOFU) semantics. For any given node, attestation may occur only once. Subsequent attestation attempts will be rejected. diff --git a/doc/plugin_server_nodeattestor_gcp_iit.md b/doc/plugin_server_nodeattestor_gcp_iit.md index 71659b3493..3a2d2756b1 100644 --- a/doc/plugin_server_nodeattestor_gcp_iit.md +++ b/doc/plugin_server_nodeattestor_gcp_iit.md @@ -8,18 +8,19 @@ This plugin requires an allow list of ProjectID from which nodes can be attested ## Configuration -| Configuration | Description | Default | -|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|---------| -| `projectid_allow_list` | List of ProjectIDs from which nodes can be attested. | | -| `use_instance_metadata` | If true, instance metadata is fetched from the Google Compute Engine API and used to augment the node selectors produced by the plugin. | false | -| `service_account_file` | Path to the service account file used to authenticate with the Google Compute Engine API | | -| `allowed_label_keys` | Instance label keys considered for selectors | | -| `allowed_metadata_keys` | Instance metadata keys considered for selectors | | -| `max_metadata_value_size` | Sets the maximum metadata value size considered by the plugin for selectors | 128 | +| Configuration | Description | Default | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------| +| `projectid_allow_list` | List of ProjectIDs from which nodes can be attested. | | +| `use_instance_metadata` | If true, instance metadata is fetched from the Google Compute Engine API and used to augment the node selectors produced by the plugin. | false | +| `service_account_file` | Path to the service account file used to authenticate with the Google Compute Engine API | | +| `allowed_label_keys` | Instance label keys considered for selectors | | +| `allowed_metadata_keys` | Instance metadata keys considered for selectors | | +| `max_metadata_value_size` | Sets the maximum metadata value size considered by the plugin for selectors | 128 | +| `agent_path_template` | A URL path portion format of Agent's SPIFFE ID. Describe in text/template format. | `"/{{ .PluginName }}/{{ .ProjectID }}/{{ .InstanceID }}"` | A sample configuration: -``` +```hcl NodeAttestor "gcp_iit" { plugin_data { projectid_allow_list = ["project-123"] @@ -31,11 +32,11 @@ A sample configuration: This plugin generates the following selectors based on information contained in the Instance Identity Token: -| Selector | Example | Description | -| -------------------------- | ------------------------------------------------------------ | ----------------------------------------- | -| `gcp_iit:project-id` | `gcp_iit:project-id:big-kahuna-123456` | ID of the project containing the instance | -| `gcp_iit:zone` | `gcp_iit:zone:us-west1-b` | Zone containing the instance | -| `gcp_iit:instance-name` | `gcp_iit:instance-name:blog-server` | Name of the instance | +| Selector | Example | Description | +|-------------------------|----------------------------------------|-------------------------------------------| +| `gcp_iit:project-id` | `gcp_iit:project-id:big-kahuna-123456` | ID of the project containing the instance | +| `gcp_iit:zone` | `gcp_iit:zone:us-west1-b` | Zone containing the instance | +| `gcp_iit:instance-name` | `gcp_iit:instance-name:blog-server` | Name of the instance | If `use_instance_metadata` is true, then the Google Compute Engine API is queried for instance metadata which is used to populate these additional selectors: @@ -62,12 +63,31 @@ corresponding selector will still have a trailing colon (i.e. `gcp_iit:label::`, `gcp_iit:metadata::`) ## Authenticating with the Google Compute Engine API + The plugin uses the Application Default Credentials to authenticate with the Google Compute Engine API, as documented by [Setting Up Authentication For Server to Server](https://cloud.google.com/docs/authentication/production). When SPIRE Server is running inside GCP, it will use the default service account credentials available to the instance it is running under. When running outside GCP, or if non-default credentials are needed, the path to the service account file containing the credentials may be specified using the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or the `service_account_file` configurable (see Configuration). The service account must have IAM permissions and Authorization Scopes granting access to the following APIs: + * [compute.instances.get](https://cloud.google.com/compute/docs/reference/rest/v1/instances/get) +## Agent Path Template + +The agent path template is a way of customizing the format of generated SPIFFE IDs for agents. +The template formatter is using Golang text/template conventions, it can reference values provided by the plugin or in a [Compute Engine identity token](https://cloud.google.com/compute/docs/instances/verifying-instance-identity#payload). + +Some useful values are: + +| Value | Description | +|----------------------------|------------------------------------------------------------------| +| .PluginName | The name of the plugin | +| .ProjectID | The ID for the project where the instance was created | +| .InstanceID | The unique ID for the instance to which this token belongs. | +| .ProjectNumber | The unique number for the project where you created the instance | +| .Zone | The zone where the instance is located | +| .InstanceCreationTimestamp | A Unix timestamp indicating when you created the instance. | + ## Security Considerations + The Instance Identity Token, which this attestor leverages to prove node identity, is available to any process running on the node by default. As a result, it is possible for non-agent code running on a node to attest to the SPIRE Server, allowing it to obtain any workload identity that the node is authorized to run. While many operators choose to configure their systems to block access to the Instance Identity Token, the SPIRE project cannot guarantee this posture. To mitigate the associated risk, the `gcp_iit` node attestor implements Trust On First Use (or TOFU) semantics. For any given node, attestation may occur only once. Subsequent attestation attempts will be rejected. diff --git a/doc/plugin_server_nodeattestor_jointoken.md b/doc/plugin_server_nodeattestor_jointoken.md index 88d030bdc5..8e26f82fc5 100644 --- a/doc/plugin_server_nodeattestor_jointoken.md +++ b/doc/plugin_server_nodeattestor_jointoken.md @@ -7,8 +7,8 @@ token must be generated by the server before it can be used to attest a node. The server uses the token to generate a SPIFFE ID with the form: -``` -spiffe:///spire/agent/join_token/ +```xml +spiffe:///spire/agent/join_token/ ``` This plugin has no configuration options. Tokens may be generated through the diff --git a/doc/plugin_server_nodeattestor_k8s_psat.md b/doc/plugin_server_nodeattestor_k8s_psat.md index 8ee81beeda..7dcd68cd95 100644 --- a/doc/plugin_server_nodeattestor_k8s_psat.md +++ b/doc/plugin_server_nodeattestor_k8s_psat.md @@ -7,8 +7,8 @@ validates the signed projected service account token provided by the agent. This validation is performed using Kubernetes [Token Review API](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#tokenreview-v1-authentication-k8s-io). In addition to validation, this API provides other useful information (namespace, service account name and pod name) that SPIRE server uses to build selectors. Kubernetes API server is also queried to get extra data like node UID, which is used to generate a SPIFFE ID with the form: -``` -spiffe:///spire/agent/k8s_psat// +```xml +spiffe:///spire/agent/k8s_psat// ``` The server does not need to be running in Kubernetes in order to perform node @@ -33,7 +33,7 @@ Each cluster in the main configuration requires the following configuration: A sample configuration for SPIRE server running inside of a Kubernetes cluster: -``` +```hcl NodeAttestor "k8s_psat" { plugin_data { clusters = { @@ -46,7 +46,7 @@ A sample configuration for SPIRE server running inside of a Kubernetes cluster: A sample configuration for SPIRE server running outside of a Kubernetes cluster: -``` +```hcl NodeAttestor "k8s_psat" { plugin_data { clusters = { @@ -75,5 +75,4 @@ This plugin generates the following selectors: The node and pod selectors are only provided for label keys in the `allowed_node_label_keys` and `allowed_pod_label_keys` configurables. - A full example of this attestor is provided in [the SPIRE examples repository](https://github.com/spiffe/spire-examples/tree/main/examples/k8s/simple_psat) diff --git a/doc/plugin_server_nodeattestor_k8s_sat.md b/doc/plugin_server_nodeattestor_k8s_sat.md index dcf59d8921..7daa1c166c 100644 --- a/doc/plugin_server_nodeattestor_k8s_sat.md +++ b/doc/plugin_server_nodeattestor_k8s_sat.md @@ -5,6 +5,7 @@ The `k8s_sat` plugin attests nodes running in inside of Kubernetes. The server validates the signed service account token provided by the agent. This validation can be done in two different ways depending on the value of the `use_token_review_api_validation` flag: + + If this value is set to `false` (default behavior), the attestor validates the token locally using the key provided in `service_account_key_file`. + If this value is set to `true`, the validation is performed using the Kubernetes [Token Review API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). @@ -13,8 +14,8 @@ you should instead consider using the `k8s_psat` attestor due to the [security c The server uses a one-time UUID provided by the agent to generate a SPIFFE ID with the form: -``` -spiffe:///spire/agent/k8s_sat// +```xml +spiffe:///spire/agent/k8s_sat// ``` The server does not need to be running in Kubernetes in order to perform node @@ -36,9 +37,9 @@ Each cluster in the main configuration requires the following configuration: | `service_account_key_file` | It is only used if `use_token_review_api_validation` is set to `false`. Path on disk to a PEM encoded file containing public keys used in validating tokens for that cluster. RSA and ECDSA keys are supported. For RSA, X509 certificates, PKCS1, and PKIX encoded public keys are accepted. For ECDSA, X509 certificates, and PKIX encoded public keys are accepted. | | | `kube_config_file` | It is only used if `use_token_review_api_validation` is set to `true`. Path to a k8s configuration file for API Server authentication. A Kubernetes configuration file must be specified if SPIRE server runs outside of the k8s cluster. If empty, SPIRE server is assumed to be running inside the cluster and in-cluster configuration is used. | "" | - A sample configuration for SPIRE server running inside or outside of a Kubernetes cluster and validating the service account token with a key file located at `"/run/k8s-certs/sa.pub"`: -``` + +```hcl NodeAttestor "k8s_sat" { plugin_data { clusters = { @@ -51,7 +52,8 @@ A sample configuration for SPIRE server running inside or outside of a Kubernete ``` A sample configuration for SPIRE server running inside of a Kubernetes cluster and validating the service account token with the kubernetes token review API: -``` + +```hcl NodeAttestor "k8s_sat" { plugin_data { clusters = { @@ -64,7 +66,8 @@ A sample configuration for SPIRE server running inside of a Kubernetes cluster a ``` A sample configuration for SPIRE server running outside of a Kubernetes cluster and validating the service account token with the kubernetes token review API: -``` + +```hcl NodeAttestor "k8s_sat" { plugin_data { clusters = { diff --git a/doc/plugin_server_nodeattestor_sshpop.md b/doc/plugin_server_nodeattestor_sshpop.md index d7915f8b76..b79bae1aa8 100644 --- a/doc/plugin_server_nodeattestor_sshpop.md +++ b/doc/plugin_server_nodeattestor_sshpop.md @@ -11,8 +11,8 @@ private key. The SPIFFE ID produced by the plugin is based on the certificate fingerprint, which is an unpadded url-safe base64 encoded sha256 hash of the certificate in openssh format. -``` -spiffe:///spire/agent/sshpop/ +```xml +spiffe:///spire/agent/sshpop/ ``` | Configuration | Description | Default | @@ -24,11 +24,11 @@ spiffe:///spire/agent/sshpop/ If both `cert_authorities` and `cert_authorities_path` are configured, the resulting set of authorized keys is the union of both sets. -### Example Config +## Example Config -##### agent.conf +### agent.conf -``` +```hcl NodeAttestor "sshpop" { plugin_data { host_cert_path = "./conf/agent/dummy_agent_ssh_key-cert.pub" @@ -37,9 +37,9 @@ If both `cert_authorities` and `cert_authorities_path` are configured, the resul } ``` -##### server.conf +### server.conf -``` +```hcl NodeAttestor "sshpop" { plugin_data { cert_authorities = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEAWPAsKJ/qMYUIBeH7BLMRCE/bkUvMHX+7OZhANk45S"] diff --git a/doc/plugin_server_nodeattestor_tpm_devid.md b/doc/plugin_server_nodeattestor_tpm_devid.md index 4e9372e485..db5614f756 100644 --- a/doc/plugin_server_nodeattestor_tpm_devid.md +++ b/doc/plugin_server_nodeattestor_tpm_devid.md @@ -3,53 +3,51 @@ *Must be used in conjunction with the agent-side tpm_devid plugin* The `tpm_devid` plugin attests nodes that own a TPM -and that have been provisioned with a DevID certificate through an out-of-band -mechanism. +and that have been provisioned with a DevID certificate through an out-of-band +mechanism. The plugin issues two challenges to the agent: -1. A proof-of-possession challenge: This is required to verify the node is in -possession of the private key that corresponds to the DevID certificate. +1. A proof-of-possession challenge: This is required to verify the node is in +possession of the private key that corresponds to the DevID certificate. Additionally, the server verifies that the DevID certificate is rooted to a trusted set of CAs. -2. A proof-of-residency challenge: This is required to prove that the DevID -key pair was generated and resides in a TPM. Additionally, the server verifies +2. A proof-of-residency challenge: This is required to prove that the DevID +key pair was generated and resides in a TPM. Additionally, the server verifies that the TPM is authentic by verifying that the endorsement certificate is rooted to a trusted set of manufacturer CAs. - The SPIFFE ID produced by the plugin is based on the certificate fingerprint, where the fingerprint is defined as the SHA1 hash of the ASN.1 DER encoding of -the identity certificate. +the identity certificate. The SPIFFE ID has the form: -``` -spiffe:///spire/agent/tpm_devid/ +```xml +spiffe:///spire/agent/tpm_devid/ ``` - -| Configuration | Description | Default | +| Configuration | Description | Default | |-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| -| `devid_ca_path` | The path to the trusted CA certificate(s) on disk to use for DevID validation. The file must contain one or more PEM blocks forming the set of trusted root CA's for chain-of-trust verification. | | -| `endorsement_ca_path` | The path to the trusted manufacturer CA certificate(s) on disk. The file must contain one or more PEM blocks forming the set of trusted manufacturer CA's for chain-of-trust verification. | | +| `devid_ca_path` | The path to the trusted CA certificate(s) on disk to use for DevID validation. The file must contain one or more PEM blocks forming the set of trusted root CA's for chain-of-trust verification. | | +| `endorsement_ca_path` | The path to the trusted manufacturer CA certificate(s) on disk. The file must contain one or more PEM blocks forming the set of trusted manufacturer CA's for chain-of-trust verification. | | A sample configuration: -``` - NodeAttestor "tpm_devid" { - plugin_data { - devid_ca_path = "/opt/spire/conf/server/devid-cacert.pem" - endorsement_ca_path = "/opt/spire/conf/server/endorsement-cacert.pem" - } - } +```hcl + NodeAttestor "tpm_devid" { + plugin_data { + devid_ca_path = "/opt/spire/conf/server/devid-cacert.pem" + endorsement_ca_path = "/opt/spire/conf/server/endorsement-cacert.pem" + } + } ``` ## Selectors -| Selector | Example | Description | +| Selector | Example | Description | |-----------------------------|-------------------------------------------------------------------|------------------------------------------------------------------------------------------| -| Subject common name | `tpm_devid:subject:cn:example.org` | The subject's common name. | -| Issuer common name | `tpm_devid:issuer:cn:authority.org` | The issuer's common name. | -| SHA1 fingerprint | `tpm_devid:fingerprint:9ba51e2643bea24e91d24bdec3a1aaf8e967b6e5` | The SHA1 fingerprint as a hex string for each cert in the PoP chain, excluding the leaf. | +| Subject common name | `tpm_devid:subject:cn:example.org` | The subject's common name. | +| Issuer common name | `tpm_devid:issuer:cn:authority.org` | The issuer's common name. | +| SHA1 fingerprint | `tpm_devid:fingerprint:9ba51e2643bea24e91d24bdec3a1aaf8e967b6e5` | The SHA1 fingerprint as a hex string for each cert in the PoP chain, excluding the leaf. | diff --git a/doc/plugin_server_nodeattestor_x509pop.md b/doc/plugin_server_nodeattestor_x509pop.md index 3dd3c31e7f..7ca860ef7d 100644 --- a/doc/plugin_server_nodeattestor_x509pop.md +++ b/doc/plugin_server_nodeattestor_x509pop.md @@ -12,8 +12,8 @@ The SPIFFE ID produced by the plugin is based on the certificate fingerprint, where the fingerprint is defined as the SHA1 hash of the ASN.1 DER encoding of the identity certificate. The SPIFFE ID has the form: -``` -spiffe:///spire/agent/x509pop/ +```xml +spiffe:///spire/agent/x509pop/ ``` | Configuration | Description | Default | @@ -24,15 +24,15 @@ spiffe:///spire/agent/x509pop/ A sample configuration: -``` - NodeAttestor "x509pop" { - plugin_data { - ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem" - - # Change the agent's SPIFFE ID format - # agent_path_template = "/cn/{{ .Subject.CommonName }}" - } - } +```hcl + NodeAttestor "x509pop" { + plugin_data { + ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem" + + # Change the agent's SPIFFE ID format + # agent_path_template = "/cn/{{ .Subject.CommonName }}" + } + } ``` ## Selectors @@ -43,6 +43,7 @@ A sample configuration: | SHA1 Fingerprint | `x509pop:ca:fingerprint:0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33` | The SHA1 fingerprint as a hex string for each cert in the PoP chain, excluding the leaf. | ## Agent Path Template + The agent path template is a way of customizing the format of generated SPIFFE IDs for agents. The template formatter is using Golang text/template conventions, it can reference values provided by the plugin or in a [golang x509.Certificate](https://pkg.go.dev/crypto/x509#Certificate) @@ -54,4 +55,4 @@ Some useful values are: | .Fingerprint | The SHA1 fingerprint of the agent's x509 certificate | | .TrustDomain | The configured trust domain | | .Subject.CommonName | The common name field of the agent's x509 certificate | -| .Subject.SerialNumber | The serial number field of the agent's x509 certificate | \ No newline at end of file +| .Subject.SerialNumber | The serial number field of the agent's x509 certificate | diff --git a/doc/plugin_server_notifier_gcs_bundle.md b/doc/plugin_server_notifier_gcs_bundle.md index 591aa52077..eb262194e1 100644 --- a/doc/plugin_server_notifier_gcs_bundle.md +++ b/doc/plugin_server_notifier_gcs_bundle.md @@ -30,7 +30,7 @@ The following configuration uploads bundle contents to the `spire-bundle.pem` object in the `my-bucket` bucket. The bundle is uploaded using Application Default Credentials available in the environment SPIRE server is running in. -``` +```hcl Notifier "gcs_bundle" { plugin_data { bucket = "my-bucket" @@ -45,7 +45,7 @@ The following configuration uploads bundle contents to the `spire-bundle.pem` object in the `my-bucket` bucket. The bundle is uploaded using Service Account credentials found in the `/path/to/service/account/file` file. -``` +```hcl Notifier "gcs_bundle" { plugin_data { bucket = "my-bucket" diff --git a/doc/plugin_server_notifier_k8sbundle.md b/doc/plugin_server_notifier_k8sbundle.md index 265a3314cc..70dabd05e7 100644 --- a/doc/plugin_server_notifier_k8sbundle.md +++ b/doc/plugin_server_notifier_k8sbundle.md @@ -13,21 +13,21 @@ The plugin accepts the following configuration options: | namespace | The namespace containing the ConfigMap | `spire` | | config_map | The name of the ConfigMap | `spire-bundle` | | config_map_key | The key within the ConfigMap for the bundle | `bundle.crt` | -| kube_config_file_path | The path on disk to the kubeconfig containing configuration to enable interaction with the Kubernetes API server. If unset, it is assumed the notifier is in-cluster and in-cluster credentials will be used. | | +| kube_config_file_path | The path on disk to the kubeconfig containing configuration to enable interaction with the Kubernetes API server. If unset, it is assumed the notifier is in-cluster and in-cluster credentials will be used. Required when configuring a remote cluster. See the `clusters` setting to configure multiple remote clusters. | | | api_service_label | If set, rotate the CA Bundle in API services with this label set to `true`. | | | webhook_label | If set, rotate the CA Bundle in validating and mutating webhooks with this label set to `true`. | | -| clusters | A list of cluster configurations. If set it can be used to configure multiple. Each cluster allows the same values as the root configuration. | | +| clusters | A list of remote cluster configurations. If set it can be used to configure multiple. Each cluster allows the same values as the root configuration. | | ## Configuring Kubernetes -The following actions are required to set up the plugin. +The following actions are required to set up the plugin: -- Bind ClusterRole or Role that can `get` and `patch` the ConfigMap to Service Account - - In the case of in-cluster SPIRE server, it is Service Account that runs the SPIRE server - - In the case of out-of-cluster SPIRE server, it is Service Account that interacts with the Kubernetes API server - - In the case of setting `webhook_label`, the ClusterRole or Role additionally needs permissions to `get`, `list`, `patch`, and `watch` `mutatingwebhookconfigurations` and `validatingwebhookconfigurations`. - - In the case of setting `api_service_label`, the ClusterRole or Role additonally needs permissions to `get`, `list`, `patch`, and `watch` `apiservices`. -- Create the ConfigMap that the plugin pushes +- Bind ClusterRole or Role that can `get` and `patch` the ConfigMap to Service Account. + - In the case of in-cluster SPIRE server, it is Service Account that runs the SPIRE Server. + - In the case of out-of-cluster SPIRE Server, it is Service Account that interacts with the Kubernetes API server. + - In the case of setting `webhook_label`, the ClusterRole or Role additionally needs permissions to `get`, `list`, `patch`, and `watch` `mutatingwebhookconfigurations` and `validatingwebhookconfigurations`. + - In the case of setting `api_service_label`, the ClusterRole or Role additionally needs permissions to `get`, `list`, `patch`, and `watch` `apiservices`. +- Create the ConfigMap that the plugin pushes. For example: @@ -95,7 +95,7 @@ rules: The following configuration pushes bundle contents from an in-cluster SPIRE server to the `bundle.crt` key in the `spire:spire-bundle` ConfigMap. -``` +```hcl Notifier "k8sbundle" { plugin_data { } @@ -108,7 +108,7 @@ The following configuration pushes bundle contents from an out-of-cluster SPIRE server to the `boostrap.crt` key in the `infra:agents` ConfigMap using the credentials found in the `/path/to/kubeconfig` file. -``` +```hcl Notifier "k8sbundle" { plugin_data { namespace = "infra" @@ -123,11 +123,12 @@ the credentials found in the `/path/to/kubeconfig` file. The following configuration pushes bundle contents from an in-cluster SPIRE server to + - The `bundle.crt` key in the `spire:spire-bundle` ConfigMap - Validating and mutating webhooks with a label of `spiffe.io/webhook: true` - API services with a label of `spiffe.io/api_service: true` -``` +```hcl Notifier "k8sbundle" { plugin_data { webhook_label = "spiffe.io/webhook" @@ -136,9 +137,9 @@ server to } ``` -### Multipe clusters +### Multiple clusters -``` +```hcl Notifier "k8sbundle" { plugin_data { # local cluster diff --git a/doc/plugin_server_upstreamauthority_aws_pca.md b/doc/plugin_server_upstreamauthority_aws_pca.md index d3636c7b77..860861162a 100644 --- a/doc/plugin_server_upstreamauthority_aws_pca.md +++ b/doc/plugin_server_upstreamauthority_aws_pca.md @@ -23,7 +23,7 @@ See [AWS Certificate Manager Private Certificate Authority](https://aws.amazon.c Sample configuration: -``` +```hcl UpstreamAuthority "aws_pca" { plugin_data { region = "us-west-2" diff --git a/doc/plugin_server_upstreamauthority_awssecret.md b/doc/plugin_server_upstreamauthority_awssecret.md index 69c77a2ed5..22c589442f 100644 --- a/doc/plugin_server_upstreamauthority_awssecret.md +++ b/doc/plugin_server_upstreamauthority_awssecret.md @@ -19,7 +19,7 @@ The plugin accepts the following configuration options: Only the region, cert_file_arn, and key_file_arn must be configured. You optionally configure the remaining fields depending on how you choose to give SPIRE Server access to the ARNs. -| If SPIRE Server Accesses the ARNs | then these additional fields are mandatory | +| If SPIRE Server Accesses the ARNs | then these additional fields are mandatory | |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| | by providing an access key id and secret access key | `access_key_id`, `secret_access_key` | | by using temporary credentials for an IAM account (*NOTE:* It is the server user's responsibility to provide a new valid token whenever the server is started) | `access_key_id`, `secret_access_key`, `secret_token` | @@ -28,13 +28,13 @@ Only the region, cert_file_arn, and key_file_arn must be configured. You optiona Because the plugin fetches the secrets from the AWS secrets manager only at startup, automatic rotation of secrets is not advised. -SPIRE Server requires that you employ a distinct Amazon Resource Name (ARN) for the CA certificate and the CA key. +SPIRE Server requires that you employ a distinct Amazon Resource Name (ARN) for the CA certificate and the CA key. -For more information on the AWS Secrets Manager, see the [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) documentation.  +For more information on the AWS Secrets Manager, see the [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) documentation. A sample configuration: -``` +```hcl UpstreamAuthority "awssecret" { plugin_data { region = "us-west-2", diff --git a/doc/plugin_server_upstreamauthority_cert_manager.md b/doc/plugin_server_upstreamauthority_cert_manager.md index c653a518d5..74feae02cd 100644 --- a/doc/plugin_server_upstreamauthority_cert_manager.md +++ b/doc/plugin_server_upstreamauthority_cert_manager.md @@ -9,13 +9,14 @@ This plugin will request a signing certificate from cert-manager via a resource. Once the referenced issuer has signed the request, the intermediate and CA bundle is retrieved by SPIRE. -# Considerations +## Considerations + This plugin requires access to a Kubernetes cluster running cert-manager and create CertificateRequests. Only issuers that have support for providing signing certificates are supported. -# Permissions +## Permissions The provided kubeconfig must include a Kubernetes client that has [create permissions for CertificateRequests](https://cert-manager.io/docs/concepts/certificaterequest/) @@ -24,7 +25,8 @@ Kubernetes client is, as this may have implications on the [approval flow](https://cert-manager.io/docs/concepts/certificaterequest/#userinfo) if running a custom approver. -# Configuration +## Configuration + This plugin requests certificates from the configured [cert-manager](https://cert-manager.io/docs/configuration/) issuer. @@ -36,7 +38,6 @@ This plugin requests certificates from the configured | issuer_kind | (Optional) The kind of the issuer to reference in CertificateRequests. Defaults to "Issuer" if empty. | | issuer_group | (Optional) The group of the issuer to reference in CertificateRequests. Defaults to "cert-manager.io" if empty. | - ```hcl UpstreamAuthority "cert-manager" { plugin_data { diff --git a/doc/plugin_server_upstreamauthority_disk.md b/doc/plugin_server_upstreamauthority_disk.md index 6ebc965f9b..4e4335af57 100644 --- a/doc/plugin_server_upstreamauthority_disk.md +++ b/doc/plugin_server_upstreamauthority_disk.md @@ -36,7 +36,7 @@ Key files must contain a single PEM encoded key. The supported key types are EC A sample configuration: -``` +```hcl UpstreamAuthority "disk" { plugin_data { cert_file_path = "conf/server/dummy_upstream_ca.crt" diff --git a/doc/plugin_server_upstreamauthority_gcp_cas.md b/doc/plugin_server_upstreamauthority_gcp_cas.md index 2ae335ab78..1939c56223 100644 --- a/doc/plugin_server_upstreamauthority_gcp_cas.md +++ b/doc/plugin_server_upstreamauthority_gcp_cas.md @@ -3,7 +3,8 @@ The `gcp_cas` plugin uses the Certificate Authority from Google Cloud Platform, known as "Certificate Authority Service" (CAS), to generate intermediate signing certificates for SPIRE Server. -# Configuration +## Configuration + The plugin has a mandatory root_cert_spec section. It is used to specify which CAs are used for signing intermediate CAs as well as being part of the trusted root bundle. If it matches multiple CAs, the earliest expiring CA is used for signing. @@ -18,7 +19,7 @@ The plugin has a mandatory root_cert_spec section. It is used to specify which C | label_key | Label key - value pair is used to filter and select the relevant certificate | | label_value | Label key - value pair is used to filter and select the relevant certificate | -##Sample configuration: +### Sample configuration ```yaml UpstreamAuthority "gcp_cas" { @@ -33,24 +34,29 @@ UpstreamAuthority "gcp_cas" { } } ``` -# What does the plugin do + +## What does the plugin do + The plugin retrieves the CAs in GCPs that are in ENABLED state and match the root cert spec parameters specified in the plugin configuration. Among the matching certificates, the CA with the earliest expiry time is selected and used to create and sign an intermediate CA. The trust bundle contains the root CAs of all the CAs in GCP that matched the root_cert_spec label -# CA Rotation +## CA Rotation + * Steady state: Config label matches CA X and CA Y in CAS; plugin has been signing with CA X and all agents are trusting CA X and CA Y. * Now create CA Z with the same label in CAS. * Disable and optionally delete CA X in CAS. * The plugin returns Y and Z's root certificates as UpstreamX509Roots. It also signs the issuing CA with Y which is now the earliest expiring CA. * This doesn't impact existing workloads because they have been trusting Y even before SPIRE started to sign with Y. -# Authentication with Google Cloud Platform +## Authentication with Google Cloud Platform + This plugin connects and authenticates with Google Cloud Platform's CAS implicitly using Application Default Credentials (ADC). The ADC mechanism is documented at . >ADC looks for service account credentials in the following order: +> >1. If the environment variable GOOGLE_APPLICATION_CREDENTIALS is set, ADC uses the service account file that the variable points to. >1. If the environment variable GOOGLE_APPLICATION_CREDENTIALS isn't set, ADC uses the service account that is attached to the resource that is running your code. >1. If the environment variable GOOGLE_APPLICATION_CREDENTIALS isn't set, and there is no service account attached to the resource that is running your code, ADC uses the default service account that Compute Engine, Google Kubernetes Engine, App Engine, Cloud Run, and Cloud Functions provide. diff --git a/doc/plugin_server_upstreamauthority_spire.md b/doc/plugin_server_upstreamauthority_spire.md index ec3780f8a3..83bdb6140a 100644 --- a/doc/plugin_server_upstreamauthority_spire.md +++ b/doc/plugin_server_upstreamauthority_spire.md @@ -23,7 +23,7 @@ These are the current experimental configurations: Sample configuration (Unix): -``` +```hcl UpstreamAuthority "spire" { plugin_data { server_address = "upstream-spire-server", @@ -35,7 +35,7 @@ Sample configuration (Unix): Sample configuration (Windows): -``` +```hcl UpstreamAuthority "spire" { plugin_data { server_address = "upstream-spire-server", diff --git a/doc/plugin_server_upstreamauthority_vault.md b/doc/plugin_server_upstreamauthority_vault.md index e06514de73..b8d3a00c1e 100644 --- a/doc/plugin_server_upstreamauthority_vault.md +++ b/doc/plugin_server_upstreamauthority_vault.md @@ -1,4 +1,4 @@ -# Upstream Authority "vault" Plugin +# Upstream Authority "vault" Plugin The vault plugin signs intermediate CA certificates for SPIRE using the Vault PKI Engine. The plugin does not support the `PublishJWTKey` RPC and is therefore not appropriate for use in nested SPIRE topologies where JWT-SVIDs are in use. @@ -7,17 +7,17 @@ The plugin does not support the `PublishJWTKey` RPC and is therefore not appropr The plugin accepts the following configuration options: -| key | type | required | description | default | -|:----|:-----|:---------|:------------|:--------| -| vault_addr | string | | The URL of the Vault server. (e.g., https://vault.example.com:8443/) | `${VAULT_ADDR}` | -| namespace | string | | Name of the Vault namespace. This is only available in the Vault Enterprise. | `${VAULT_NAMESPACE}` | -| pki_mount_point | string | | Name of the mount point where PKI secret engine is mounted | pki | -| ca_cert_path | string | | Path to a CA certificate file used to verify the Vault server certificate. Only PEM format is supported. | `${VAULT_CACERT}` | -| insecure_skip_verify | bool | | If true, vault client accepts any server certificates | false | -| cert_auth | struct | | Configuration for the Client Certificate authentication method | | -| token_auth | struct | | Configuration for the Token authentication method | | -| approle_auth | struct | | Configuration for the AppRole authentication method | | -| k8s_auth | struct | | Configuration for the Kubernetes authentication method | | +| key | type | required | description | default | +|:---------------------|:-------|:---------|:-----------------------------------------------------------------------------------------------------------|:---------------------| +| vault_addr | string | | The URL of the Vault server. (e.g., ) | `${VAULT_ADDR}` | +| namespace | string | | Name of the Vault namespace. This is only available in the Vault Enterprise. | `${VAULT_NAMESPACE}` | +| pki_mount_point | string | | Name of the mount point where PKI secret engine is mounted | pki | +| ca_cert_path | string | | Path to a CA certificate file used to verify the Vault server certificate. Only PEM format is supported. | `${VAULT_CACERT}` | +| insecure_skip_verify | bool | | If true, vault client accepts any server certificates | false | +| cert_auth | struct | | Configuration for the Client Certificate authentication method | | +| token_auth | struct | | Configuration for the Token authentication method | | +| approle_auth | struct | | Configuration for the AppRole authentication method | | +| k8s_auth | struct | | Configuration for the Kubernetes authentication method | | The plugin supports **Client Certificate**, **Token** and **AppRole** authentication methods. @@ -29,7 +29,8 @@ The [`ca_ttl` SPIRE Server configurable](https://github.com/spiffe/spire/blob/ma To configure the TTL value, tune the engine. e.g. -``` + +```shell $ vault secrets tune -max-lease-ttl=8760h pki ``` @@ -43,12 +44,12 @@ path "pki/root/sign-intermediate" { ## Client Certificate Authentication -| key | type | required | description | default | -|:----|:-----|:---------|:------------|:--------| -| cert_auth_mount_point | string | | Name of the mount point where TLS certificate auth method is mounted | cert | -| cert_auth_role_name | string | | Name of the Vault role. If given, the plugin authenticates against only the named role. Default to trying all roles. | | -| client_cert_path | string | | Path to a client certificate file. Only PEM format is supported. | `${VAULT_CLIENT_CERT}` | -| client_key_path | string | | Path to a client private key file. Only PEM format is supported. | `${VAULT_CLIENT_KEY}` | +| key | type | required | description | default | +|:----------------------|:-------|:---------|:---------------------------------------------------------------------------------------------------------------------|:-----------------------| +| cert_auth_mount_point | string | | Name of the mount point where TLS certificate auth method is mounted | cert | +| cert_auth_role_name | string | | Name of the Vault role. If given, the plugin authenticates against only the named role. Default to trying all roles. | | +| client_cert_path | string | | Path to a client certificate file. Only PEM format is supported. | `${VAULT_CLIENT_CERT}` | +| client_key_path | string | | Path to a client private key file. Only PEM format is supported. | `${VAULT_CLIENT_KEY}` | ```hcl UpstreamAuthority "vault" { @@ -79,12 +80,12 @@ path "pki/root/sign-intermediate" { } } ``` -## Token Authentication -| key | type | required | description | default | -|:----|:-----|:---------|:------------|:--------| -| token | string | | Token string to set into "X-Vault-Token" header | `${VAULT_TOKEN}` | +## Token Authentication +| key | type | required | description | default | +|:------|:-------|:---------|:------------------------------------------------|:-----------------| +| token | string | | Token string to set into "X-Vault-Token" header | `${VAULT_TOKEN}` | ```hcl UpstreamAuthority "vault" { @@ -100,13 +101,14 @@ path "pki/root/sign-intermediate" { } } ``` + ## AppRole Authentication -| key | type | required | description | default | -|:----|:-----|:---------|:------------|:--------| -| approle_auth_mount_point | string | | Name of the mount point where the AppRole auth method is mounted | approle | -| approle_id |string | | An identifier of AppRole | `${VAULT_APPROLE_ID}` | -| approle_secret_id | string | | A credential of AppRole | `${VAULT_APPROLE_SECRET_ID}` | +| key | type | required | description | default | +|:-------------------------|:-------|:---------|:-----------------------------------------------------------------|:-----------------------------| +| approle_auth_mount_point | string | | Name of the mount point where the AppRole auth method is mounted | approle | +| approle_id | string | | An identifier of AppRole | `${VAULT_APPROLE_ID}` | +| approle_secret_id | string | | A credential of AppRole | `${VAULT_APPROLE_SECRET_ID}` | ```hcl UpstreamAuthority "vault" { @@ -132,11 +134,11 @@ path "pki/root/sign-intermediate" { ## Kubernetes Authentication -| key | type | required | description | default | -|:----|:-----|:---------|:------------|:--------| -| k8s_auth_mount_point | string | | Name of the mount point where the Kubernetes auth method is mounted | kubernetes | -| k8s_auth_role_name | string |✔| Name of the Vault role. The plugin authenticates against the named role | | -| token_path | string |✔| Path to the Kubernetes Service Account Token to use authentication with the Vault | | +| key | type | required | description | default | +|:---------------------|:-------|:---------|:----------------------------------------------------------------------------------|:-----------| +| k8s_auth_mount_point | string | | Name of the mount point where the Kubernetes auth method is mounted | kubernetes | +| k8s_auth_role_name | string | ✔ | Name of the Vault role. The plugin authenticates against the named role | | +| token_path | string | ✔ | Path to the Kubernetes Service Account Token to use authentication with the Vault | | ```hcl UpstreamAuthority "vault" { diff --git a/doc/scaling_spire.md b/doc/scaling_spire.md index be8a1b83c7..e3e681c2c1 100644 --- a/doc/scaling_spire.md +++ b/doc/scaling_spire.md @@ -1,4 +1,6 @@ -# Scalability +# Scaling SPIRE + +## Scalability A SPIRE deployment has the capacity to be changed in size or scale to accommodate a growing amount of workloads. A SPIRE deployment is composed of a number of one or more SPIRE Servers that share a replicated datastore, or conversely, a set of SPIRE servers in the same trust domain, and at least one SPIRE Agent, but typically more than one. @@ -6,7 +8,7 @@ Deployments range in size. A single SPIRE Server may accommodate a number of Age To support larger numbers of Agents and Workloads within a given deployment (tens of thousands or hundreds of thousands of nodes), the number of SPIRE Servers can be scaled horizontally. With multiple servers, the amount of computational work that a SPIRE Server performs is distributed between all SPIRE Server instances. In addition to additional capacity, the use of more than one SPIRE Server instance eliminates single points of failure to achieve high availability. -## SPIRE Servers in High Availability Mode +### SPIRE Servers in High Availability Mode ![Diagram of High Availability](/doc/images/ha_mode.png) @@ -16,7 +18,7 @@ The datastore is where SPIRE Server persists dynamic configuration information s In High Availability mode, each server maintains its own Certificate Authority, which may be either self-signed certificates or an intermediate certificate off of a shared root authority (i.e. when configured with an UpstreamAuthority). -# Choosing a SPIRE Deployment Topology +## Choosing a SPIRE Deployment Topology There are three main SPIRE deployment topologies: @@ -26,7 +28,7 @@ There are three main SPIRE deployment topologies: Factors such as administrative domain boundaries, number of workloads, availability requirements, number of cloud vendors, and authentication requirements determine the appropriate topology for your environment, as explained below. -## Single Trust Domain +### Single Trust Domain ![Diagram of Single Trust Domain](/doc/images/single_trust_domain.png) @@ -34,8 +36,7 @@ A single trust domain is best suited for individual environments or environments However, when deploying a single SPIRE trust domain to span regions, platforms, and cloud provider environments, there is a level of complexity associated with managing a shared datastore across geographically dispersed locations or across cloud provider boundaries. Under these circumstances when a deployment grows to span multiple environments, a solution to address the use of a shared datastore over a single trust domain is to configure SPIRE Servers in a nested topology. -## Nested SPIRE - +### Nested SPIRE ![Diagram of Nested SPIRE](/doc/images/nested_spire.png) @@ -51,7 +52,7 @@ The Nested topology is well suited for multi-cloud deployments. Due to the abili Complementary to scaling SPIRE Servers horizontally for high availability and load-balancing, a nested topology may be used as a containment strategy to segment failure domains. -## Federated SPIRE +### Federated SPIRE ![Diagram of Federated SPIRE](/doc/images/federated_spire.png) @@ -61,13 +62,13 @@ Another use case is SPIFFE interoperability between organizations, such as betwe These multiple trust domain and interoperability use cases both require a well-defined, interoperable method for a Workload in one trust domain to authenticate a Workload in a different trust domain. Trust between the different trust domains is established by first authenticating the respective bundle endpoint, followed by retrieval of the foreign trust domain bundle via the authenticated endpoint. -For additional detail on how this is achieved, refer to the following SPIFFE spec that describes the mechanism: https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#5-spiffe-bundle-endpoint +For additional detail on how this is achieved, refer to the following SPIFFE spec that describes the mechanism: -For a tutorial on configuring Federated SPIRE, refer to: https://github.com/spiffe/spire-tutorials/tree/main/docker-compose/federation +For a tutorial on configuring Federated SPIRE, refer to: -# Interaction with External Systems +## Interaction with External Systems -## Federation with "SPIFFE-Compatible" Systems +### Federation with "SPIFFE-Compatible" Systems ![Diagram of Federated with SPIFFE-Compatible Systems](/doc/images/spiffe_compatible.png) @@ -75,7 +76,7 @@ SPIFFE identity issuers can federate with other SPIFFE identity issuers that exp For example, in current Istio, all applications on the service mesh are in the same trust domain thus share a common root of trust. There may be more than one service mesh, or applications in the service mesh communicating to external services that need to be authenticated. The use of Federation enables SPIFFE-compatible systems such as multiple Istio service meshes to securely establish trust for secure cross-mesh and off-mesh communications. -## Federation with OIDC-Provider Systems +### Federation with OIDC-Provider Systems ![Diagram of Federated with SPIFFE-Compatible Systems](/doc/images/oidc_federation.png) @@ -84,11 +85,11 @@ SPIRE has a feature to programmatically authenticate on behalf of identified wor The SPIRE OIDC Discovery Provider retrieves a WebPKI certificate using the ACME protocol, which it uses to secure an endpoint that serves an OIDC compatible JWKS bundle and a standard OIDC discovery document. The remote OIDC authenticated service needs then to be configured to locate the endpoint and qualify the WebPKI service. Once this configuration is in place, the remote system’s IAM policies and roles can be set to map to specific SPIFFE IDs. The workload, in turn, will talk to the OIDC-authenticated system by sending a JWT-SVID. The target system then fetches a JWKS from the pre-defined URI which is served by the OIDC Discovery Provider. The target system uses the JWKS file to validate the JWT-SVID, and if the SPIFFE ID contained within the JWT-SVID is authorized to access the requested resource, it serves the request. The workload is then able to access the foreign remote service without possessing any credentials provided by it. For a configuration reference on the OIDC Discovery Provider, see: -https://github.com/spiffe/spire/tree/main/support/oidc-discovery-provider + -For a detailed tutorial on configuring OIDC Federation to Amazon Web Services, refer to: https://spiffe.io/spire/try/oidc-federation-aws/ +For a detailed tutorial on configuring OIDC Federation to Amazon Web Services, refer to: -# Deployment Sizing Considerations +## Deployment Sizing Considerations Factors to consider when sizing a SPIRE deployment for optimum performance include, but are not limited to, the following: diff --git a/doc/spire_agent.md b/doc/spire_agent.md index c5e0732655..444fb6d0fa 100644 --- a/doc/spire_agent.md +++ b/doc/spire_agent.md @@ -71,14 +71,15 @@ This may be useful for templating configuration files, for example across differ | `named_pipe_name` | Pipe name to bind the SPIRE Agent API named pipe (Windows only) | \spire-agent\public\api | ### Initial trust bundle configuration + The agent needs an initial trust bundle in order to connect securely to the SPIRE server. There are three options: + 1. If the `trust_bundle_path` option is used, the agent will read the initial trust bundle from the file at that path. You need to copy or share the file before starting the SPIRE agent. 2. If the `trust_bundle_url` option is used, the agent will read the initial trust bundle from the specified URL. **The URL must start with `https://` for security, and the server must have a valid certificate (verified with the system trust store).** This can be used to rapidly deploy SPIRE agents without having to manually share a file. Keep in mind the contents of the URL need to be kept up to date. 3. If the `insecure_bootstrap` option is set to `true`, then the agent will not use an initial trust bundle. It will connect to the SPIRE server without authenticating it. This is not a secure configuration, because a man-in-the-middle attacker could control the SPIRE infrastructure. It is included because it is a useful option for testing and development. Only one of these three options may be set at a time. - ### SDS Configuration | Configuration | Description | Default | @@ -89,7 +90,9 @@ Only one of these three options may be set at a time. | `disable_spiffe_cert_validation` | Disable Envoy SDS custom validation | false | ### Profiling Names + These are the available profiles that can be set in the `profiling_freq` configuration value: + - `goroutine` - `threadcreate` - `heap` @@ -166,6 +169,25 @@ the following flags are available: | `-trustBundleUrl` | URL to download the SPIRE server CA bundle | | | `-trustDomain` | The trust domain that this agent belongs to (should be no more than 255 characters) | | +#### Running SPIRE Agent as a Windows service + +On Windows platform, SPIRE Agent can optionally be run as a Windows service. When running as a Windows service, the only command supported is the `run` command. + +_Note: SPIRE does not automatically create the service in the system, it must be created by the user. +When starting the service, all the arguments to execute SPIRE Agent with the `run` command must be passed as service arguments._ + +##### Example to create the SPIRE Agent Windows service + +```bash +> sc.exe create spire-agent binpath=c:\spire\bin\spire-agent.exe +``` + +##### Example to run the SPIRE Agent Windows service + +```bash +> sc.exe start spire-agent run -config c:\spire\conf\agent\agent.conf +``` + ### `spire-agent api fetch` Calls the workload API to fetch an X509-SVID. This command is aliased to `spire-agent api fetch x509`. @@ -280,13 +302,15 @@ plugins { } } ``` + ## Delegated Identity API -SPIRE agent has support for Delegated Identity API. This API is intended for use cases where a (authorized) workload wants access to the X509-SVIDs and bundles on behalf of another workload. A list of authorized delegates SPIFFE IDs of workloads are defined for this purpose. The API is served over the same endpoint address as the admin API. Based on workload's selectors, a (authorized) workload could be subscribed to get X509-SVIDs and federated bundles from the registered entries that match the provided selectors. +The Delegated Identity API allows an authorized (i.e. delegated) workload to obtain SVIDs and bundles on behalf of workloads that cannot be attested by SPIRE Agent directly. The authorized workload does so by providing SPIRE Agent the selectors that would normally be obtained during workload attestation. The Delegated Identity API is served over the admin API endpoint. -In order to use this API, you shall configure the `admin_socket_path` and `authorized_delegates` (SPIFFE ID list of authorized delegates identities), as the following example: +To enable the Delegated Identity API, configure the admin API endpoint address and the list of SPIFFE IDs for authorized delegates. For example: Unix systems: + ```hcl agent { trust_domain = "example.org" @@ -300,6 +324,7 @@ agent { ``` Windows: + ```hcl agent { trust_domain = "example.org" @@ -351,7 +376,7 @@ _Note: A user with `cluster-admin` privileges is required in order to apply thes Actions performed by pods are controlled by Security Context Constraints (SCC's) and every pod that is admitted is assigned a particular SCC depending on range of conditions. The following custom SCC with the name `spire` can be used to enable the necessary host level access needed by the Spire Agent -``` +```yaml allowHostDirVolumePlugin: true allowHostIPC: true allowHostNetwork: true @@ -402,7 +427,7 @@ Workloads can be granted access to Security Context Constraints through Role Bas In order to leverage the `spire` SCC, a _ClusterRole_ leveraging `use` verb referencing the SCC must be created: -``` +```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -425,7 +450,7 @@ Finally, associate the `system:openshift:scc:spire` _ClusterRole_ to the `spire- _Note:_ Create the `spire` namespace if it does exist prior to applying the following policy. -``` +```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: @@ -445,5 +470,5 @@ As SCC's are applied at pod admission time, remove any existing Spire Agent pods ## Further reading -* [SPIFFE Reference Implementation Architecture](https://docs.google.com/document/d/1nV8ZbYEATycdFhgjTB619pwIvamzOjU6l0SyBGbzbo4/edit#) -* [Design Document: SPIFFE Reference Implementation (SRI)](https://docs.google.com/document/d/1RZnBfj8I5xs8Yi_BPEKBRp0K3UnIJYTDg_31rfTt4j8/edit#) +- [SPIFFE Reference Implementation Architecture](https://docs.google.com/document/d/1nV8ZbYEATycdFhgjTB619pwIvamzOjU6l0SyBGbzbo4/edit#) +- [Design Document: SPIFFE Reference Implementation (SRI)](https://docs.google.com/document/d/1RZnBfj8I5xs8Yi_BPEKBRp0K3UnIJYTDg_31rfTt4j8/edit#) diff --git a/doc/spire_server.md b/doc/spire_server.md index b209a1dc95..33a86b1f25 100644 --- a/doc/spire_server.md +++ b/doc/spire_server.md @@ -4,13 +4,13 @@ This document is a configuration reference for SPIRE Server. It includes informa ## Plugin types -| Type | Description | -|:---------------|:------------| -| DataStore | Provides persistent storage and HA features. **Note:** Pluggability for the DataStore is no longer supported. Only the built-in SQL plugin can be used. | -| KeyManager | Implements both signing and key storage logic for the server's signing operations. Useful for leveraging hardware-based key operations. | -| NodeAttestor | Implements validation logic for nodes attempting to assert their identity. Generally paired with an agent plugin of the same type. | -| UpstreamAuthority | Allows SPIRE server to integrate with existing PKI systems. | -| Notifier | Notified by SPIRE server for certain events that are happening or have happened. For events that are happening, the notifier can advise SPIRE server on the outcome. | +| Type | Description | +|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DataStore | Provides persistent storage and HA features. **Note:** Pluggability for the DataStore is no longer supported. Only the built-in SQL plugin can be used. | +| KeyManager | Implements both signing and key storage logic for the server's signing operations. Useful for leveraging hardware-based key operations. | +| NodeAttestor | Implements validation logic for nodes attempting to assert their identity. Generally paired with an agent plugin of the same type. | +| UpstreamAuthority | Allows SPIRE server to integrate with existing PKI systems. | +| Notifier | Notified by SPIRE server for certain events that are happening or have happened. For events that are happening, the notifier can advise SPIRE server on the outcome. | ## Built-in plugins @@ -48,33 +48,35 @@ SPIRE configuration files may be represented in either HCL or JSON. Please see t If the -expandEnv flag is passed to SPIRE, `$VARIABLE` or `${VARIABLE}` style environment variables are expanded before parsing. This may be useful for templating configuration files, for example across different trust domains, or for inserting secrets like database connection passwords. -| Configuration | Description | Default | -|:--------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------| -| `admin_ids` | SPIFFE IDs that, when present in a caller's X509-SVID, grant that caller admin privileges. The admin IDs must reside in the same trust domain as the server and need not have a corresponding admin registration entry with the server. | | -| `agent_ttl` | The TTL to use for agent SVIDs | The value of `default_svid_ttl` | -| `audit_log_enabled` | If true, enables audit logging | false | -| `bind_address` | IP address or DNS name of the SPIRE server | 0.0.0.0 | -| `bind_port` | HTTP Port number of the SPIRE server | 8081 | -| `ca_key_type` | The key type used for the server CA (both X509 and JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | ec-p256 (the JWT key type can be overridden by `jwt_key_type`) | -| `ca_subject` | The Subject that CA certificates should use (see below) | | -| `ca_ttl` | The default CA/signing key TTL | 24h | -| `data_dir` | A directory the server can use for its runtime | | -| `default_svid_ttl` | The default SVID TTL | 1h | -| `experimental` | The experimental options that are subject to change or removal (see below) | | -| `federation` | Bundle endpoints configuration section used for [federation](#federation-configuration) | | -| `jwt_key_type` | The key type used for the server CA (JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | The value of `ca_key_type` or ec-p256 if not defined | -| `jwt_issuer` | The issuer claim used when minting JWT-SVIDs | | -| `log_file` | File to write logs to | | -| `log_level` | Sets the logging level <DEBUG|INFO|WARN|ERROR> | INFO | -| `log_format` | Format of logs, <text|json> | text | -| `omit_x509svid_uid` | If true, the subject on X509-SVIDs will not contain the unique ID attribute (deprecated) | false | -| `profiling_enabled` | If true, enables a [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint | false | -| `profiling_freq` | Frequency of dumping profiling data to disk. Only enabled when `profiling_enabled` is `true` and `profiling_freq` > 0. | | -| `profiling_names` | List of profile names that will be dumped to disk on each profiling tick, see [Profiling Names](#profiling-names) | | -| `profiling_port` | Port number of the [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint. Only used when `profiling_enabled` is `true`. | | -| `ratelimit` | Rate limiting configurations, usually used when the server is behind a load balancer (see below) | | -| `socket_path` | Path to bind the SPIRE Server API socket to (Unix only) | /tmp/spire-server/private/api.sock | -| `trust_domain` | The trust domain that this server belongs to (should be no more than 255 characters) | | +| Configuration | Description | Default | +|:------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------| +| `admin_ids` | SPIFFE IDs that, when present in a caller's X509-SVID, grant that caller admin privileges. The admin IDs must reside in the same trust domain as the server and need not have a corresponding admin registration entry with the server. | | +| `agent_ttl` | The TTL to use for agent SVIDs | The value of `default_svid_ttl` | +| `audit_log_enabled` | If true, enables audit logging | false | +| `bind_address` | IP address or DNS name of the SPIRE server | 0.0.0.0 | +| `bind_port` | HTTP Port number of the SPIRE server | 8081 | +| `ca_key_type` | The key type used for the server CA (both X509 and JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | ec-p256 (the JWT key type can be overridden by `jwt_key_type`) | +| `ca_subject` | The Subject that CA certificates should use (see below) | | +| `ca_ttl` | The default CA/signing key TTL | 24h | +| `data_dir` | A directory the server can use for its runtime | | +| `default_svid_ttl` | The default SVID TTL. This field is deprecated in favor of default_x509_svid_ttl and default_jwt_svid_ttl and will be removed in a future version. | 1h | +| `default_x509_svid_ttl` | The default X509-SVID TTL (overrides `default_svid_ttl` if set) | 1h | +| `default_jwt_svid_ttl` | The default JWT-SVID TTL (overrides `default_svid_ttl` if set) | 5m | +| `experimental` | The experimental options that are subject to change or removal (see below) | | +| `federation` | Bundle endpoints configuration section used for [federation](#federation-configuration) | | +| `jwt_key_type` | The key type used for the server CA (JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | The value of `ca_key_type` or ec-p256 if not defined | +| `jwt_issuer` | The issuer claim used when minting JWT-SVIDs | | +| `log_file` | File to write logs to | | +| `log_level` | Sets the logging level <DEBUG|INFO|WARN|ERROR> | INFO | +| `log_format` | Format of logs, <text|json> | text | +| `omit_x509svid_uid` | If true, the subject on X509-SVIDs will not contain the unique ID attribute (deprecated) | false | +| `profiling_enabled` | If true, enables a [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint | false | +| `profiling_freq` | Frequency of dumping profiling data to disk. Only enabled when `profiling_enabled` is `true` and `profiling_freq` > 0. | | +| `profiling_names` | List of profile names that will be dumped to disk on each profiling tick, see [Profiling Names](#profiling-names) | | +| `profiling_port` | Port number of the [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint. Only used when `profiling_enabled` is `true`. | | +| `ratelimit` | Rate limiting configurations, usually used when the server is behind a load balancer (see below) | | +| `socket_path` | Path to bind the SPIRE Server API socket to (Unix only) | /tmp/spire-server/private/api.sock | +| `trust_domain` | The trust domain that this server belongs to (should be no more than 255 characters) | | | ca_subject | Description | Default | |:----------------------------|--------------------------------|----------------| @@ -102,9 +104,10 @@ This may be useful for templating configuration files, for example across differ | `rego_path` | File to retrieve OPA rego policy for authorization. | | | `policy_data_path` | File to retrieve databindings for policy evaluation. | | - ### Profiling Names + These are the available profiles that can be set in the `profiling_freq` configuration value: + - `goroutine` - `threadcreate` - `heap` @@ -144,9 +147,10 @@ SPIRE Server can be configured to federate with others SPIRE Servers living in d _Note: static relationships override dynamic relationships. If you need to configure dynamic relationships, see the [`federation`](#spire-server-federation-create) command. Static relationships are not reflected in the `federation` command._ -Configuring a federated trust domain allows a trust domain to authenticate identities issued by other SPIFFE authorities, allowing workloads in one trust domain to securely autenticate workloads in a foreign trust domain. +Configuring a federated trust domain allows a trust domain to authenticate identities issued by other SPIFFE authorities, allowing workloads in one trust domain to securely authenticate workloads in a foreign trust domain. A key element to achieve federation is the use of SPIFFE bundle endpoints, these are resources (represented by URLs) that serve a copy of a trust bundle for a trust domain. Using the `federation` section you will be able to set up SPIRE as a SPIFFE bundle endpoint server and also configure the federated trust domains that this SPIRE Server will fetch bundles from. + ```hcl server { . @@ -174,10 +178,12 @@ server { } } ``` + The `federation.bundle_endpoint` section is optional and is used to set up a SPIFFE bundle endpoint server in SPIRE Server. The `federation.federates_with` section is also optional and is used to configure the federation relationships with foreign trust domains. This section is used for each federated trust domain that SPIRE Server will periodically fetch the bundle. ### Configuration options for `federation.bundle_endpoint` + This optional section contains the configurables used by SPIRE Server to expose a bundle endpoint. | Configuration | Description | @@ -190,7 +196,7 @@ This optional section contains the configurables used by SPIRE Server to expose | Configuration | Description | Default | |---------------|---------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| -| directory_url | Directory endpoint URL | https://acme-v02.api.letsencrypt.org/directory | +| directory_url | Directory endpoint URL | | | domain_name | Domain for which the certificate manager tries to retrieve new certificates | | | email | Contact email address. This is used by CAs, such as Let's Encrypt, to notify about problems with issued certificates | | | tos_accepted | ACME Terms of Service acceptance. If not true, and the provider requires acceptance, then certificate retrieval will fail | false | @@ -249,6 +255,25 @@ Most of the configuration file above options have identical command-line counter | `-socketPath` | Path to bind the SPIRE Server API socket to | | | `-trustDomain` | The trust domain that this server belongs to (should be no more than 255 characters) | | +#### Running SPIRE Server as a Windows service + +On Windows platform, SPIRE Server can optionally be run as a Windows service. When running as a Windows service, the only command supported is the `run` command. + +_Note: SPIRE does not automatically create the service in the system, it must be created by the user. +When starting the service, all the arguments to execute SPIRE Server with the `run` command must be passed as service arguments._ + +##### Example to create the SPIRE Server Windows service + +```bash +> sc.exe create spire-server binpath=c:\spire\bin\spire-server.exe +``` + +##### Example to run the SPIRE Server Windows service + +```bash +> sc.exe start spire-server run -config c:\spire\conf\server\server.conf +``` + ### `spire-server token generate` Generates one node join token and creates a registration entry for it. This token can be used to @@ -265,40 +290,44 @@ human-readable registration entry name in addition to the token-based ID. Creates registration entries. -| Command | Action | Default | -|:-----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------| -| `-admin` | If set, the SPIFFE ID in this entry will be granted access to the Server APIs | | -| `-data` | Path to a file containing registration data in JSON format (optional, if specified, other flags related with entry information must be omitted). If set to '-', read the JSON from stdin. | | -| `-dns` | A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once | | -| `-downstream` | A boolean value that, when set, indicates that the entry describes a downstream SPIRE server | | -| `-entryExpiry` | An expiry, from epoch in seconds, for the resulting registration entry to be pruned from the datastore. Please note that this is a data management feature and not a security feature (optional). | | -| `-federatesWith` | A list of trust domain SPIFFE IDs representing the trust domains this registration entry federates with. A bundle for that trust domain must already exist | | -| `-node` | If set, this entry will be applied to matching nodes rather than workloads | | -| `-parentID` | The SPIFFE ID of this record's parent. | | -| `-selector` | A colon-delimited type:value selector used for attestation. This parameter can be used more than once, to specify multiple selectors that must be satisfied. | | -| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | -| `-spiffeID` | The SPIFFE ID that this record represents and will be set to the SVID issued. | | -| `-ttl` | A TTL, in seconds, for any SVID issued as a result of this record. | The TTL configured with `default_svid_ttl` | +| Command | Action | Default | +|:-----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------| +| `-admin` | If set, the SPIFFE ID in this entry will be granted access to the Server APIs | | +| `-data` | Path to a file containing registration data in JSON format (optional, if specified, other flags related with entry information must be omitted). If set to '-', read the JSON from stdin. | | +| `-dns` | A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once | | +| `-downstream` | A boolean value that, when set, indicates that the entry describes a downstream SPIRE server | | +| `-entryExpiry` | An expiry, from epoch in seconds, for the resulting registration entry to be pruned from the datastore. Please note that this is a data management feature and not a security feature (optional). | | +| `-federatesWith` | A list of trust domain SPIFFE IDs representing the trust domains this registration entry federates with. A bundle for that trust domain must already exist | | +| `-node` | If set, this entry will be applied to matching nodes rather than workloads | | +| `-parentID` | The SPIFFE ID of this record's parent. | | +| `-selector` | A colon-delimited type:value selector used for attestation. This parameter can be used more than once, to specify multiple selectors that must be satisfied. | | +| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | +| `-spiffeID` | The SPIFFE ID that this record represents and will be set to the SVID issued. | | +| `-ttl` | A TTL, in seconds, for any SVID issued as a result of this record. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version. | The TTL configured with `default_svid_ttl` | +| `-x509SVIDTTL` | A TTL, in seconds, for any X509-SVID issued as a result of this record. Overrides `-ttl` value. | The TTL configured with `default_x509_svid_ttl` | +| `-jwtSVIDTTL` | A TTL, in seconds, for any JWT-SVID issued as a result of this record. Overrides `-ttl` value. | The TTL configured with `default_jwt_svid_ttl` | | `-storeSVID` | A boolean value that, when set, indicates that the resulting issued SVID from this entry must be stored through an SVIDStore plugin | ### `spire-server entry update` Updates registration entries. -| Command | Action | Default | -|:-----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------| -| `-admin` | If true, the SPIFFE ID in this entry will be granted access to the Server APIs | | -| `-data` | Path to a file containing registration data in JSON format (optional, if specified, other flags related with entry information must be omitted). If set to '-', read the JSON from stdin. | | -| `-dns` | A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once | | -| `-downstream` | A boolean value that, when set, indicates that the entry describes a downstream SPIRE server | | -| `-entryExpiry` | An expiry, from epoch in seconds, for the resulting registration entry to be pruned | | -| `-entryID` | The Registration Entry ID of the record to update | | -| `-federatesWith` | A list of trust domain SPIFFE IDs representing the trust domains this registration entry federates with. A bundle for that trust domain must already exist | | -| `-parentID` | The SPIFFE ID of this record's parent. | | -| `-selector` | A colon-delimited type:value selector used for attestation. This parameter can be used more than once, to specify multiple selectors that must be satisfied. | | -| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | -| `-spiffeID` | The SPIFFE ID that this record represents and will be set to the SVID issued. | | -| `-ttl` | A TTL, in seconds, for any SVID issued as a result of this record. | The TTL configured with `default_svid_ttl` | +| Command | Action | Default | +|:-----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------| +| `-admin` | If true, the SPIFFE ID in this entry will be granted access to the Server APIs | | +| `-data` | Path to a file containing registration data in JSON format (optional, if specified, other flags related with entry information must be omitted). If set to '-', read the JSON from stdin. | | +| `-dns` | A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once | | +| `-downstream` | A boolean value that, when set, indicates that the entry describes a downstream SPIRE server | | +| `-entryExpiry` | An expiry, from epoch in seconds, for the resulting registration entry to be pruned | | +| `-entryID` | The Registration Entry ID of the record to update | | +| `-federatesWith` | A list of trust domain SPIFFE IDs representing the trust domains this registration entry federates with. A bundle for that trust domain must already exist | | +| `-parentID` | The SPIFFE ID of this record's parent. | | +| `-selector` | A colon-delimited type:value selector used for attestation. This parameter can be used more than once, to specify multiple selectors that must be satisfied. | | +| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | +| `-spiffeID` | The SPIFFE ID that this record represents and will be set to the SVID issued. | | +| `-ttl` | A TTL, in seconds, for any SVID issued as a result of this record. This flag is deprecated in favor of x509SVIDTTL and jwtSVIDTTL and will be removed in a future version. | The TTL configured with `default_svid_ttl` | +| `-x509SVIDTTL` | A TTL, in seconds, for any X509-SVID issued as a result of this record. Overrides `-ttl` value. | The TTL configured with `default_x509_svid_ttl` | +| `-jwtSVIDTTL` | A TTL, in seconds, for any JWT-SVID issued as a result of this record. Overrides `-ttl` value. | The TTL configured with `default_jwt_svid_ttl` | | `storeSVID` | A boolean value that, when set, indicates that the resulting issued SVID from this entry must be stored through an SVIDStore plugin | ### `spire-server entry count` @@ -328,7 +357,7 @@ Displays configured registration entries. | `-entryID` | The Entry ID of the record to show. | | | `-federatesWith` | SPIFFE ID of a trust domain an entry is federate with. Can be used more than once | | | `-parentID` | The Parent ID of the records to show. | | -| `-selector` | A colon-delimeted type:value selector. Can be used more than once to specify multiple selectors. | | +| `-selector` | A colon-delimited type:value selector. Can be used more than once to specify multiple selectors. | | | `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | | `-spiffeID` | The SPIFFE ID of the records to show. | | @@ -513,25 +542,25 @@ Typically, you may want at least: Mints an X509-SVID. -| Command | Action | Default | -|:--------------|:---------------------------------------------------------------------|:-------------------------------------------| -| `-dns` | A DNS name that will be included in SVID. Can be used more than once | | -| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | -| `-spiffeID` | The SPIFFE ID of the X509-SVID | | -| `-ttl` | The TTL of the X509-SVID | The TTL configured with `default_svid_ttl` | -| `-write` | Directory to write output to instead of stdout | | +| Command | Action | Default | +|:--------------|:---------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| +| `-dns` | A DNS name that will be included in SVID. Can be used more than once | | +| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | +| `-spiffeID` | The SPIFFE ID of the X509-SVID | | +| `-ttl` | The TTL of the X509-SVID | First non-zero value from `Entry.x509_svid_ttl`, `Entry.ttl`, `default_x509_svid_ttl`, `default_svid_ttl`, `1h` | +| `-write` | Directory to write output to instead of stdout | | ### `spire-server jwt mint` Mints a JWT-SVID. -| Command | Action | Default | -|:--------------|:-----------------------------------------------------------------------------|:-----------------------------------| -| `-audience` | Audience claim that will be included in the SVID. Can be used more than once | | -| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | -| `-spiffeID` | The SPIFFE ID of the JWT-SVID | | -| `-ttl` | The TTL of the JWT-SVID | | -| `-write` | File to write token to instead of stdout | | +| Command | Action | Default | +|:--------------|:-----------------------------------------------------------------------------|:------------------------------------------------------------------------------------------| +| `-audience` | Audience claim that will be included in the SVID. Can be used more than once | | +| `-socketPath` | Path to the SPIRE Server API socket | /tmp/spire-server/private/api.sock | +| `-spiffeID` | The SPIFFE ID of the JWT-SVID | | +| `-ttl` | The TTL of the JWT-SVID | First non-zero value from `Entry.jwt_svid_ttl`, `Entry.ttl`, `default_jwt_svid_ttl`, `5m` | +| `-write` | File to write token to instead of stdout | | ## JSON object for `-data` @@ -560,7 +589,8 @@ server { bind_port = "8081" log_level = "INFO" data_dir = "/opt/spire/.data/" - default_svid_ttl = "6h" + default_x509_svid_ttl = "6h" + default_jwt_svid_ttl = "5m" ca_ttl = "72h" ca_subject { country = ["US"] @@ -595,5 +625,5 @@ plugins { ## Further reading -* [SPIFFE Reference Implementation Architecture](https://docs.google.com/document/d/1nV8ZbYEATycdFhgjTB619pwIvamzOjU6l0SyBGbzbo4/edit#) -* [Design Document: SPIFFE Reference Implementation (SRI)](https://docs.google.com/document/d/1RZnBfj8I5xs8Yi_BPEKBRp0K3UnIJYTDg_31rfTt4j8/edit#) +- [SPIFFE Reference Implementation Architecture](https://docs.google.com/document/d/1nV8ZbYEATycdFhgjTB619pwIvamzOjU6l0SyBGbzbo4/edit#) +- [Design Document: SPIFFE Reference Implementation (SRI)](https://docs.google.com/document/d/1RZnBfj8I5xs8Yi_BPEKBRp0K3UnIJYTDg_31rfTt4j8/edit#) diff --git a/doc/supported_integrations.md b/doc/supported_integrations.md index 6ca46dae1f..9a36122534 100644 --- a/doc/supported_integrations.md +++ b/doc/supported_integrations.md @@ -18,5 +18,5 @@ the last minor version that supports it (v1.16). ## Kubernetes -The SPIRE project currently supports Kubernetes 1.18 through 1.20. Later +The SPIRE project currently supports Kubernetes 1.18 through 1.21. Later versions may also work but are not explicitly exercised by integration tests. diff --git a/doc/telemetry.md b/doc/telemetry.md index 191d418c98..e2275fa7a9 100644 --- a/doc/telemetry.md +++ b/doc/telemetry.md @@ -8,7 +8,7 @@ The following metrics are emitted: | Type | Keys | Labels | Description | |--------------|---------------------------------------------|-------------------|---------------------------------------------------------------------------------------| -| Call Counter | `rpc`, ``, `` | | Call counters over the SPIRE Server RPCs | +| Call Counter | `rpc`, ``, `` | | Call counters over the [SPIRE Server RPCs](https://github.com/spiffe/spire-api-sdk). | | Call Counter | `ca`, `manager`, `bundle`, `prune` | | The CA manager is pruning a bundle. | | Counter | `ca`, `manager`, `bundle`, `pruned` | | The CA manager has successfully pruned a bundle. | | Call Counter | `ca`, `manager`, `jwt_key`, `prepare` | | The CA manager is preparing a JWT Key. | @@ -19,12 +19,12 @@ The following metrics are emitted: | Call Counter | `datastore`, `bundle`, `create` | | The Datastore is creating a bundle. | | Call Counter | `datastore`, `bundle`, `delete` | | The Datastore is deleting a bundle. | | Call Counter | `datastore`, `bundle`, `fetch` | | The Datastore is fetching a bundle. | -| Call Counter | `datastore`, `bundle`, `list` | | The Datastore is listing bundles. | -| Call Counter | `datastore`, `bundle`, `prune` | | The Datastore is pruning a bundle. | -| Call Counter | `datastore`, `bundle`, `set` | | The Datastore is setting a bundle. | -| Call Counter | `datastore`, `bundle`, `update` | | The Datastore is updating a bundle. | -| Call Counter | `datastore`, `join_token`, `create` | | The Datastore is creating a join token. | -| Call Counter | `datastore`, `join_token`, `delete` | | The Datastore is deleting a join token. | +| Call Counter | `datastore`, `bundle`, `list` | | The Datastore is listing bundles. | +| Call Counter | `datastore`, `bundle`, `prune` | | The Datastore is pruning a bundle. | +| Call Counter | `datastore`, `bundle`, `set` | | The Datastore is setting a bundle. | +| Call Counter | `datastore`, `bundle`, `update` | | The Datastore is updating a bundle. | +| Call Counter | `datastore`, `join_token`, `create` | | The Datastore is creating a join token. | +| Call Counter | `datastore`, `join_token`, `delete` | | The Datastore is deleting a join token. | | Call Counter | `datastore`, `join_token`, `fetch` | | The Datastore is fetching a join token. | | | Call Counter | `datastore`, `join_token`, `prune` | | The Datastore is pruning join tokens. | | | Call Counter | `datastore`, `node`, `count` | | The Datastore is counting nodes. | @@ -56,27 +56,55 @@ The following metrics are emitted: ## SPIRE Agent -| Type | Keys | Labels | Description | -|--------------|--------------------------------------------|------------|---------------------------------------------------------------------------| -| Call Counter | `rpc`, ``, `` | | Call counters over the SPIRE Agent RPCs | -| Call Counter | `agent_key_manager`, `generate_key_pair` | | The KeyManager is generating a key pair. | -| Call Counter | `agent_key_manager`, `fetch_private_key` | | The KeyManager is fetching a private key. | -| Call Counter | `agent_key_manager`, `store_private_key` | | The KeyManager is storing a private key. | -| Call Counter | `agent_svid`, `rotate` | | The Agent's SVID is being rotated. | -| Sample | `cache_manager`, `expiring_svids` | | The number of expiring SVIDs that the Cache Manager has. | -| Sample | `cache_manager`, `outdated_svids` | | The number of outdated SVIDs that the Cache Manager has. | -| Call Counter | `manager`, `sync`, `fetch_entries_updates` | | The Sync Manager is fetching entries updates. | -| Call Counter | `manager`, `sync`, `fetch_svids_updates` | | The Sync Manager is fetching SVIDs updates. | -| Call Counter | `node`, `attestor`, `new_svid` | | The Node Attestor is calling to get an SVID. | -| Counter | `sds_api`, `connections` | | The SDS API has successfully established a connection. | -| Gauge | `sds_api`, `connections` | | The number of active connection that the SDS API has. | -| Counter | `workload_api`, `bundles_update`, `jwt` | | The Workload API has successfully updated a JWT bundle. | -| Counter | `workload_api`, `connection` | | The Workload API has successfully established a new connection. | -| Gauge | `workload_api`, `connections` | | The number of active connections that the Workload API has. | -| Sample | `workload_api`, `discovered_selectors` | | The number of selectors discovered during a workload attestation process. | -| Call Counter | `workload_api`, `workload_attestation` | | The Workload API is performing a workload attestation. | -| Call Counter | `workload_api`, `workload_attestor` | `attestor` | The Workload API is invoking a given attestor. | -| Gauge | `started` | `version` | The version of the Agent. | -| Gauge | `uptime_in_ms` | | The uptime of the Agent in milliseconds. | +| Type | Keys | Labels | Description | +|--------------|--------------------------------------------|------------|---------------------------------------------------------------------------------------| +| Call Counter | `rpc`, ``, `` | | Call counters over the [SPIRE Agent RPCs](). | +| Call Counter | `agent_key_manager`, `generate_key_pair` | | The KeyManager is generating a key pair. | +| Call Counter | `agent_key_manager`, `fetch_private_key` | | The KeyManager is fetching a private key. | +| Call Counter | `agent_key_manager`, `store_private_key` | | The KeyManager is storing a private key. | +| Call Counter | `agent_svid`, `rotate` | | The Agent's SVID is being rotated. | +| Sample | `cache_manager`, `expiring_svids` | | The number of expiring SVIDs that the Cache Manager has. | +| Sample | `cache_manager`, `outdated_svids` | | The number of outdated SVIDs that the Cache Manager has. | +| Call Counter | `manager`, `sync`, `fetch_entries_updates` | | The Sync Manager is fetching entries updates. | +| Call Counter | `manager`, `sync`, `fetch_svids_updates` | | The Sync Manager is fetching SVIDs updates. | +| Call Counter | `node`, `attestor`, `new_svid` | | The Node Attestor is calling to get an SVID. | +| Counter | `sds_api`, `connections` | | The SDS API has successfully established a connection. | +| Gauge | `sds_api`, `connections` | | The number of active connection that the SDS API has. | +| Counter | `workload_api`, `bundles_update`, `jwt` | | The Workload API has successfully updated a JWT bundle. | +| Counter | `workload_api`, `connection` | | The Workload API has successfully established a new connection. | +| Gauge | `workload_api`, `connections` | | The number of active connections that the Workload API has. | +| Sample | `workload_api`, `discovered_selectors` | | The number of selectors discovered during a workload attestation process. | +| Call Counter | `workload_api`, `workload_attestation` | | The Workload API is performing a workload attestation. | +| Call Counter | `workload_api`, `workload_attestor` | `attestor` | The Workload API is invoking a given attestor. | +| Gauge | `started` | `version` | The version of the Agent. | +| Gauge | `uptime_in_ms` | | The uptime of the Agent in milliseconds. | -Note: These are the keys and labels that SPIRE emits, but the format of the metric once ingested could vary depending on the metric collector. E.g. once in StatsD, the metric emitted when rotating an Agent SVID (`agent_svid`, `rotate`) can be found as `spire_agent_agent_svid_rotate_internal_host-agent-0`, where `host-agent-0` is the hostname and `spire-agent` is the service name. +Note: These are the keys and labels that SPIRE emits, but the format of the +metric once ingested could vary depending on the metric collector. For example, +in StatsD, the metric emitted when rotating an Agent SVID (`agent_svid`, +`rotate`) can be found as +`spire_agent_agent_svid_rotate_internal_host-agent-0`, where `host-agent-0` is +the hostname and `spire-agent` is the service name. + +## Call Counters + +Call counters are aggregate metric types that emit several metrics related to +the issuance of a "call" to a method or RPC. The following metrics are +produced for a call counter: + +- A counter representing the number of calls using the call counter key +- A sample of the elapsed time for the call using the call counter + key+`".elapsed_time"` + +Additionally, the metrics emitted above each carry a `status` label (in +addition to any other labels for specific to the individual call counter) that +holds the [gRPC status code](https://pkg.go.dev/google.golang.org/grpc/codes#Code) +of the call. + +For example, a successful invocation of the SPIRE Server `AttestAgent` RPC +would produce the following metrics: + +```text +spire_server.rpc.agent.v1.agent.attest_agent:1|c|#status:OK +spire_server.rpc.agent.v1.agent.attest_agent.elapsed_time:1.045773|ms|#status:OK +``` diff --git a/doc/telemetry_config.md b/doc/telemetry_config.md index 00a4df924e..d934875d73 100644 --- a/doc/telemetry_config.md +++ b/doc/telemetry_config.md @@ -1,6 +1,7 @@ -## Telemetry configuration +# Telemetry configuration If telemetry is desired, it may be configured by using a dedicated `telemetry { ... }` section. The following metrics collectors are currently supported: + - Prometheus - Statsd - DogStatsd @@ -13,7 +14,7 @@ You may use all, some, or none of the collectors. The following collectors suppo - DogStatsd - M3 -### Telemetry configuration syntax +## Telemetry configuration syntax | Configuration | Type | Description | Default | |-------------------|---------------|---------------------------------------------------------------|---------| @@ -27,30 +28,34 @@ You may use all, some, or none of the collectors. The following collectors suppo | `AllowedLabels` | `[]string` | A list of metric labels to allow, with '.' as the separator | | | `BlockedLabels` | `[]string` | A list of metric labels to block, with '.' as the separator | | -#### `Prometheus` +### `Prometheus` | Configuration | Type | Description | |---------------|----------|------------------------| | `host` | `string` | Prometheus server host | | `port` | `int` | Prometheus server port | -#### `DogStatsd` +### `DogStatsd` + | Configuration | Type | Description | |---------------|----------|-------------------| | `address` | `string` | DogStatsd address | -#### `Statsd` +### `Statsd` + | Configuration | Type | Description | |---------------|----------|----------------| | `address` | `string` | Statsd address | -#### `M3` +### `M3` + | Configuration | Type | Description | |---------------|----------|----------------------------------------------| | `address` | `string` | M3 address | | `env` | `string` | M3 environment, e.g. `production`, `staging` | -#### `In-Mem` +### `In-Mem` + | Configuration | Type | Description | Default | |---------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `enabled` | `bool` | Enable this collector. This flag is deprecated and will be removed in a future release. To disable in-memory telemetry collection omit the InMem configuration block entirely. | `false` | @@ -85,6 +90,6 @@ telemetry { } ``` -### Supported metrics +## Supported metrics See the [Telemetry document](telemetry.md) for a list of all the supported metrics. diff --git a/doc/upgrading.md b/doc/upgrading.md index 9388a06848..4123bda90c 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -1,39 +1,50 @@ # Managing Upgrades/Downgrades + This guide describes how to upgrade your SPIRE deployment, as well as the compatibility guarantees that SPIRE users can expect. ## SPIRE Versioning + SPIRE versions are expressed as **x.y.z**, where **x** is the major version, **y** is the minor version, and **z** is the patch version, following Semantic Versioning terminology. The last pre-1.0 versions are 0.12.x, which as an exception have compatibility warranties with 1.0.x. Versions prior to 0.12.0 are not compatible with 1.0.x. ### SPIRE Server Compatibility + Version skew within a SPIRE Server cluster is supported within +/- 1 minor version. In other words, the newest and oldest SPIRE Server instances in any given cluster must be within one minor version of each other. As an exception, versions 0.12.x are compatible with 1.0.x versions. Example 1 (0.12.x exception): + * Newest SPIRE Server instance is at 1.0.3 * Other SPIRE Server instances are supported at 1.0.x and 0.12.x Example 2: + * Newest SPIRE Server instance is at 1.2.3 * Other SPIRE Server instances are supported at 1.2.x and 1.1.x ### SPIRE Agent Compatibility + SPIRE Agents must not be newer than the oldest SPIRE Server that they communicate with, and may be up to one minor version older. As an exception, SPIRE Agent versions 0.12.x are compatible with SPIRE Server versions 1.0.x. Example 1 (0.12.x exception): + * SPIRE Servers are at both 1.0.3 and 1.0.2 * SPIRE Agents are supported at 0.12.0 through 1.0.2 Example 2: + * SPIRE Servers are at both 1.2.3 and 1.2.2 * SPIRE Agents are supported at 1.1.0 through 1.2.2 ### SPIRE Plugin Compatibility + SPIRE plugins generally follow the same overall guarantees as all other SPIRE components with small exception for changes made to external plugins outside of SPIRE's control. #### Configuration and Behavior Compatibility + A built-in plugin undergoing a backwards incompatible change (e.g. change to configuration semantics, change to selectors produced, etc.) will log a warning but otherwise maintain backwards compatibility for one minor version after the change is introduced, giving operators time to adopt requisite changes. SPIRE cannot make any guarantees around configuration or behavior compatibility for external plugins. #### Interface Compatibility + When a breaking change is introduced to a plugin interface, existing plugins compiled against the old interface will still continue to function for one minor version release cycle to give operators time to adopt requisite changes. SPIRE will log warnings to make operators aware of the change. ## Supported Upgrade Paths @@ -43,6 +54,7 @@ The supported version skew between SPIRE Servers and agents has implications on SPIRE Server and agent instances may be upgraded in a rolling fashion. For example, if upgrading from 1.1.1 to 1.2.3: + * Upgrade SPIRE Server instances from 1.1.1 to 1.2.3 one instance at a time * Ensure that the SPIRE Server cluster is operating as expected * Upgrade SPIRE Agent instances from 1.1.1 to 1.2.3 one instance at a time or in batches @@ -54,6 +66,7 @@ Note that while a rolling upgrade is highly recommended, it is not strictly requ SPIRE supports downgrading in the event that a problem is encountered while rolling out an upgrade. Since agents can't be newer than the oldest server they communicate with, it is necessary to first downgrade agents before downgrading servers, assuming that the agents have already been upgraded. For this reason, it is a good idea to ensure that the upgraded SPIRE Servers are operating as expected prior to upgrading the agents. For example, if downgrading from version 1.2.3 to 1.1.1: + * Downgrade SPIRE Agent instances from 1.2.3 to 1.1.1 one at a time or in batches * Downgrade SPIRE Server instances from 1.2.3 to 1.1.1 one at a time diff --git a/examples/README.md b/examples/README.md index 91664a2b54..9e43371260 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,3 @@ # Examples have been moved -The examples that lived here have moved to a dedicated repository. Please visit https://github.com/spiffe/spire-examples for maintained SPIRE integration and deployment examples. +The examples that lived here have moved to a dedicated repository. Please visit for maintained SPIRE integration and deployment examples. diff --git a/go.mod b/go.mod index 3071aa99c2..0f83c5e3c4 100644 --- a/go.mod +++ b/go.mod @@ -3,221 +3,358 @@ module github.com/spiffe/spire go 1.19 require ( - cloud.google.com/go/secretmanager v1.7.0 - cloud.google.com/go/security v1.8.0 - cloud.google.com/go/storage v1.27.0 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 + cloud.google.com/go/iam v0.9.0 + cloud.google.com/go/kms v1.7.0 + cloud.google.com/go/secretmanager v1.9.0 + cloud.google.com/go/security v1.10.0 + cloud.google.com/go/storage v1.28.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 - github.com/GoogleCloudPlatform/cloudsql-proxy v1.32.0 - github.com/InVisionApp/go-health/v2 v2.1.2 - github.com/InVisionApp/go-logger v1.0.1 + github.com/GoogleCloudPlatform/cloudsql-proxy v1.33.1 github.com/Microsoft/go-winio v0.6.0 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 github.com/armon/go-metrics v0.4.1 - github.com/aws/aws-sdk-go-v2 v1.16.16 - github.com/aws/aws-sdk-go-v2/config v1.17.4 - github.com/aws/aws-sdk-go-v2/credentials v1.12.17 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.14 - github.com/aws/aws-sdk-go-v2/service/acmpca v1.18.0 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.0 + github.com/aws/aws-sdk-go-v2 v1.17.3 + github.com/aws/aws-sdk-go-v2/config v1.18.3 + github.com/aws/aws-sdk-go-v2/credentials v1.13.3 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 + github.com/aws/aws-sdk-go-v2/service/acmpca v1.19.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.77.0 github.com/aws/aws-sdk-go-v2/service/iam v1.18.16 - github.com/aws/aws-sdk-go-v2/service/kms v1.18.8 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.0 - github.com/aws/aws-sdk-go-v2/service/sts v1.16.16 + github.com/aws/aws-sdk-go-v2/service/kms v1.19.0 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 github.com/blang/semver/v4 v4.0.0 github.com/cenkalti/backoff/v3 v3.2.2 - github.com/docker/docker v20.10.18+incompatible + github.com/docker/docker v20.10.22+incompatible github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 github.com/go-logr/logr v1.2.3 - github.com/go-sql-driver/mysql v1.6.0 - github.com/gofrs/uuid v4.3.0+incompatible + github.com/go-sql-driver/mysql v1.7.0 + github.com/gofrs/uuid v4.3.1+incompatible github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.9 + github.com/google/go-containerregistry v0.12.1 github.com/google/go-tpm v0.3.3 - github.com/google/go-tpm-tools v0.3.9 - github.com/googleapis/gax-go/v2 v2.5.1 + github.com/google/go-tpm-tools v0.3.10 + github.com/googleapis/gax-go/v2 v2.7.0 github.com/gorilla/handlers v1.5.1 - github.com/hashicorp/go-hclog v1.3.1 - github.com/hashicorp/go-plugin v1.4.5 + github.com/hashicorp/go-hclog v1.4.0 + github.com/hashicorp/go-plugin v1.4.6 github.com/hashicorp/hcl v1.0.1-0.20190430135223-99e2f22d1c94 - github.com/hashicorp/vault/api v1.8.1 - github.com/hashicorp/vault/sdk v0.6.0 + github.com/hashicorp/vault/api v1.8.2 + github.com/hashicorp/vault/sdk v0.6.2 github.com/imdario/mergo v0.3.13 github.com/imkira/go-observer v1.0.3 github.com/jinzhu/gorm v1.9.16 github.com/lib/pq v1.10.7 - github.com/mattn/go-sqlite3 v1.14.15 - github.com/mitchellh/cli v1.1.4 - github.com/open-policy-agent/opa v0.45.0 - github.com/prometheus/client_golang v1.13.0 - github.com/shirou/gopsutil/v3 v3.22.9 + github.com/mattn/go-sqlite3 v1.14.16 + github.com/mitchellh/cli v1.1.5 + github.com/open-policy-agent/opa v0.47.4 + github.com/prometheus/client_golang v1.14.0 + github.com/shirou/gopsutil/v3 v3.22.12 + github.com/sigstore/cosign v1.13.1 + github.com/sigstore/rekor v1.0.1 + github.com/sigstore/sigstore v1.4.6 github.com/sirupsen/logrus v1.9.0 - github.com/spiffe/go-spiffe/v2 v2.0.1-0.20220414143532-2ed460a8b9d3 - github.com/spiffe/spire-api-sdk v1.2.5-0.20220608195902-84fd618158c9 + github.com/spiffe/go-spiffe/v2 v2.1.1 + github.com/spiffe/spire-api-sdk v1.2.5-0.20221020001527-5895a0279944 github.com/spiffe/spire-plugin-sdk v1.4.1-0.20220912221658-c42ab2d657f6 - github.com/stretchr/testify v1.8.0 - github.com/uber-go/tally/v4 v4.1.3 + github.com/stretchr/testify v1.8.1 + github.com/uber-go/tally/v4 v4.1.4 github.com/zeebo/errs v1.3.0 - golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220909164309-bea034e7d591 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/sys v0.0.0-20220907062415-87db552b00fd - golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 - google.golang.org/api v0.98.0 - google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 - google.golang.org/grpc v1.50.0 + golang.org/x/crypto v0.4.0 + golang.org/x/net v0.5.0 + golang.org/x/sync v0.1.0 + golang.org/x/sys v0.4.0 + golang.org/x/time v0.3.0 + google.golang.org/api v0.105.0 + google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 + google.golang.org/grpc v1.51.0 google.golang.org/protobuf v1.28.1 gopkg.in/square/go-jose.v2 v2.6.0 - k8s.io/api v0.25.2 - k8s.io/apimachinery v0.25.2 - k8s.io/client-go v0.25.2 - k8s.io/kube-aggregator v0.23.3 - k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed - sigs.k8s.io/controller-runtime v0.13.0 + k8s.io/api v0.26.0 + k8s.io/apimachinery v0.26.0 + k8s.io/client-go v0.26.0 + k8s.io/kube-aggregator v0.26.0 + k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 + sigs.k8s.io/controller-runtime v0.14.1 ) require ( - cloud.google.com/go v0.104.0 // indirect - cloud.google.com/go/compute v1.9.0 // indirect - cloud.google.com/go/iam v0.3.0 // indirect + bitbucket.org/creachadair/shell v0.0.7 // indirect + cloud.google.com/go v0.107.0 // indirect + cloud.google.com/go/compute v1.13.0 // indirect + cloud.google.com/go/compute/metadata v0.2.2 // indirect + cloud.google.com/go/longrunning v0.3.0 // indirect + github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect + github.com/Azure/azure-sdk-for-go v67.1.0+incompatible // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.27 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect + github.com/Azure/go-autorest/autorest v0.11.28 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect github.com/DataDog/datadog-go v3.2.0+incompatible // indirect - github.com/Masterminds/goutils v1.1.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.1 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/ThalesIgnite/crypto11 v1.2.5 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect + github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect + github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect + github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect + github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect + github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect + github.com/alibabacloud-go/openapi-util v0.0.11 // indirect + github.com/alibabacloud-go/tea v1.1.18 // indirect + github.com/alibabacloud-go/tea-utils v1.4.4 // indirect + github.com/alibabacloud-go/tea-xml v1.1.2 // indirect + github.com/aliyun/credentials-go v1.2.3 // indirect github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.21 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.11.20 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.2 // indirect - github.com/aws/smithy-go v1.13.3 // indirect + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.11.25 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 // indirect + github.com/aws/smithy-go v1.13.5 // indirect + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795 // indirect + github.com/benbjohnson/clock v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect + github.com/clbanning/mxj/v2 v2.5.6 // indirect + github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect + github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect + github.com/containerd/stargz-snapshotter/estargz v0.12.1 // indirect + github.com/coreos/go-oidc/v3 v3.4.0 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/docker/cli v20.10.20+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.13.0 // indirect - github.com/felixge/httpsnoop v1.0.2 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fullstorydev/grpcurl v1.8.7 // indirect github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-chi/chi v4.1.2+incompatible // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.5 // indirect - github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/loads v0.21.2 // indirect + github.com/go-openapi/runtime v0.24.2 // indirect + github.com/go-openapi/spec v0.20.7 // indirect + github.com/go-openapi/strfmt v0.21.3 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/validate v0.22.0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt v3.2.1+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/certificate-transparency-go v1.1.3 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-github/v45 v45.2.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-sev-guest v0.4.1 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/logger v1.1.1 // indirect + github.com/google/trillian v1.5.0 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-retryablehttp v0.7.1 // 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.6 // indirect + github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // 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/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.3.2 // indirect - github.com/jhump/protoreflect v1.9.0 // indirect + github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect + github.com/jhump/protoreflect v1.14.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jonboulle/clockwork v0.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.11 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect - github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/run v1.0.0 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect - github.com/pierrec/lz4 v2.5.2+incompatible // indirect - github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pborman/uuid v1.2.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/posener/complete v1.2.3 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/spf13/cast v1.3.1 // indirect + github.com/sigstore/fulcio v0.6.0 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.13.0 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect - github.com/tklauser/go-sysconf v0.3.10 // indirect - github.com/tklauser/numcpus v0.4.0 // indirect + github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect + github.com/thales-e-security/pool v0.0.2 // indirect + github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/tjfoc/gmsm v1.3.2 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect + github.com/transparency-dev/merkle v0.0.1 // indirect github.com/twmb/murmur3 v1.1.6 // indirect + github.com/urfave/cli v1.22.7 // indirect + github.com/vbatts/tar-split v0.11.2 // indirect + github.com/xanzy/go-gitlab v0.73.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - go.opencensus.io v0.23.0 // indirect + go.etcd.io/bbolt v1.3.6 // indirect + go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect + go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 // indirect + go.etcd.io/etcd/v3 v3.6.0-alpha.0 // indirect + go.mongodb.org/mongo-driver v1.10.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0 // indirect + go.opentelemetry.io/otel v1.10.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0 // indirect + go.opentelemetry.io/otel/sdk v1.10.0 // indirect + go.opentelemetry.io/otel/trace v1.10.0 // indirect + go.opentelemetry.io/proto/otlp v0.19.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.23.0 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.12 // indirect - golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/oauth2 v0.2.0 // indirect + golang.org/x/term v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect + golang.org/x/tools v0.2.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect + gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.25.0 // indirect - k8s.io/component-base v0.25.0 // indirect - k8s.io/klog/v2 v2.70.1 // indirect - k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect + k8s.io/apiextensions-apiserver v0.26.0 // indirect + k8s.io/component-base v0.26.0 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/release-utils v0.7.3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index a055bcfda5..6fcae1fc01 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,14 @@ +bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= +bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= +bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -15,6 +21,7 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -26,54 +33,303 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= 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/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.17.0/go.mod h1:pUlbH9kNOnp6ayShsqKLB6w49z14ILAaq0hrjh93Ajw= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.9.0 h1:ED/FP4xv8GJw63v556/ASNc1CeeLUO2Bs8nzaHchkHg= -cloud.google.com/go/compute v1.9.0/go.mod h1:lWv1h/zUWTm/LozzfTJhBSkd6ShQq8la8VeeuOEGxfY= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k= +cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= 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/datastore v1.5.0/go.mod h1:RGUNM0FFAVkYA94BLTxoXBgfIyY1Riq67TwaBXH0lwc= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.9.0 h1:bK6Or6mxhuL8lnj1i9j0yMo2wE/IeTO2cWlfUrf/TZs= +cloud.google.com/go/iam v0.9.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.7.0 h1:8FCf8C7qfOuSr6YzOQ4RGjJvswSRFeOpur3nHOlJbio= +cloud.google.com/go/kms v1.7.0/go.mod h1:k2UdVoNIHLJi/Rnng6dN0vlq7lS3jHSDiZasft+gmYE= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= 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/secretmanager v1.7.0 h1:EAPaaxMs1gtdyxK5UN8KfD5tnDBZiFoSroRfjV3EgQU= -cloud.google.com/go/secretmanager v1.7.0/go.mod h1:20dYAPbj+H4+pXdBRN2z77yugQJJ30UF2kL9OWPs+L0= -cloud.google.com/go/security v1.8.0 h1:linnRc3/gJYDfKbAtNixVQ52+66DpOx5MmCz0NNxal8= +cloud.google.com/go/pubsub v1.11.0-beta.schemas/go.mod h1:llNLsvx+RnsZJoY481TzC1XcdB2hWdR6gSWM5O4vgfs= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0 h1:xE6uXljAC1kCR8iadt9+/blg1fvSbmenlsDN4fT9gqw= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0 h1:KSKzzJMyUoMRQzcz7azIgqAUqxo7rmQ5rYvimMhikqg= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs= +cloud.google.com/go/spanner v1.18.0/go.mod h1:LvAjUXPeJRGNuGpikMULjhLj/t9cRvdc+fxRoLiugXA= +cloud.google.com/go/spanner v1.31.0/go.mod h1:ztDJVUZgEA2xc7HjSNQG+d+2L0bOSsw876/5Hnr78U8= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= 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.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.15.0/go.mod h1:mjjQMoxxyGH7Jr8K5qrx6N2O0AHsczI61sMNn03GIZI= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY= +contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= +contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= +contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= +contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= +contrib.go.opencensus.io/exporter/stackdriver v0.13.12/go.mod h1:mmxnWlrvrFdpiOHOhxBaVi1rkc0WOqhgfknj4Yg0SeQ= +contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= +contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4 h1:pqrAR74b6EoR4kcxF7L7Wg2B8Jgil9UUZtMvxhEFqWo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 h1:8+4G8JaejP8Xa6W46PzJEwisNgBXMvFcz78N6zG/ARw= +github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0/go.mod h1:GgeIE+1be8Ivm7Sh4RgwI42aTtC9qrcj+Y9Y6CjJhJs= +github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= +github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= +github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v46.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v67.1.0+incompatible h1:oziYcaopbnIKfM69DL05wXdypiqfrUKdxUKrKpynJTw= +github.com/Azure/azure-sdk-for-go v67.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 h1:sVW/AFBTGyJxDaMYlq0ct3jUXTtj12tQ6zE2GZUgVQw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 h1:t/W5MYAuQy81cvM8VUNfRLzhtKpXhVUAN7Cd7KVbTyc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0/go.mod h1:NBanQUfSWiWn3QEpWDTCU0IjBECKOYvl2R8xdRtMtiM= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5xuRL7NZgHameVYF6BurY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 h1:/Di3vB4sNeQ+7A8efjUVENvyB945Wruvstucqp7ZArg= @@ -83,73 +339,141 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= -github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= +github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 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.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -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/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest v0.11.6/go.mod h1:V6p3pKZx1KKkJubbxnDWrzNhEIfOy/pTGasLqzHIPHs= +github.com/Azure/go-autorest/autorest v0.11.8/go.mod h1:V6p3pKZx1KKkJubbxnDWrzNhEIfOy/pTGasLqzHIPHs= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +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.4/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.20 h1:gJ3E98kMpFB1MFqQCvA1yFab8vthOeD4VlFRQULxahg= -github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk= +github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.2/go.mod h1:q98IH4qgc3eWM4/WOeR5+YPmBuy8Lq0jNRDwSM0CuFk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.1/go.mod h1:JfDgiIO1/RPu6z42AdQTyjOoCM2MFhLqSBDvMEkDgcg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE= -github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= +github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= +github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 h1:VgSJlZH5u0k2qxSpqyghcFQKmvYckj46uymKK5XzkBM= +github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5qMFKx9DugEg3+uQSDCdbYPr5s9vBTrL9P8TpqOU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.32.0 h1:647YHw0ZJ3Uu5xlkytf1li7dqJ9mHg9zabuKdZP0vYU= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.32.0/go.mod h1:FjoDxLvxFAbnXFuUKkzM7rY66YaU/YHezlau786y9hs= -github.com/InVisionApp/go-health/v2 v2.1.2 h1:rWTIgU3XdMTn/oBJgIrCnrso1pHcI65biN+CUOpknq0= -github.com/InVisionApp/go-health/v2 v2.1.2/go.mod h1:Iz2FZRfK3sJecRvGCIgyBsKOjILdKTdLGiGFaO+JDYc= -github.com/InVisionApp/go-logger v1.0.1 h1:WFL19PViM1mHUmUWfsv5zMo379KSWj2MRmBlzMFDRiE= -github.com/InVisionApp/go-logger v1.0.1/go.mod h1:+cGTDSn+P8105aZkeOfIhdd7vFO5X1afUHcjvanY0L8= -github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.33.1 h1:h1qByrLm6Q80nfvIGE5FHdJbvGloDOagO6o0N6QGPkk= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.33.1/go.mod h1:n3KDPrdaY2p9Nr0B1allAdjYArwIpXQcitNbsS/Qiok= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.2.0 h1:P1ekkbuU73Ui/wS0nK1HOM37hh4xdfZo485UPf8rc+Y= -github.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI= +github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.2.1 h1:n6EPaDyLSvCEa3frruQvAiHuNp2dhBlMSmkEr+HuzGc= +github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E= +github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.2/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/cr-20160607 v1.0.1 h1:WEnP1iPFKJU74ryUKh/YDPHoxMZawqlPajOymyNAkts= +github.com/alibabacloud-go/cr-20160607 v1.0.1/go.mod h1:QHeKZtZ3F3FOE+/uIXCBAp8POwnUYekpLwr1dtQa5r0= +github.com/alibabacloud-go/cr-20181201 v1.0.10 h1:B60f6S1imsgn2fgC6X6FrVNrONDrbCT0NwYhsJ0C9/c= +github.com/alibabacloud-go/cr-20181201 v1.0.10/go.mod h1:VN9orB/w5G20FjytoSpZROqu9ZqxwycASmGqYUJSoDc= +github.com/alibabacloud-go/darabonba-openapi v0.1.12/go.mod h1:sTAjsFJmVsmcVeklL9d9uDBlFsgl43wZ6jhI6BHqHqU= +github.com/alibabacloud-go/darabonba-openapi v0.1.14/go.mod h1:w4CosR7O/kapCtEEMBm3JsQqWBU/CnZ2o0pHorsTWDI= +github.com/alibabacloud-go/darabonba-openapi v0.1.18 h1:3eUVmAr7WCJp7fgIvmCd9ZUyuwtJYbtUqJIed5eXCmk= +github.com/alibabacloud-go/darabonba-openapi v0.1.18/go.mod h1:PB4HffMhJVmAgNKNq3wYbTUlFvPgxJpTzd1F5pTuUsc= +github.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/endpoint-util v1.1.1 h1:ZkBv2/jnghxtU0p+upSU0GGzW1VL9GQdZO3mcSUTUy8= +github.com/alibabacloud-go/endpoint-util v1.1.1/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.0.9/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.0.10/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.0.11 h1:iYnqOPR5hyEEnNZmebGyRMkkEJRWUEjDiiaOHZ5aNhA= +github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.18 h1:+6GJ06eu5Cr/Mkj09vWrf6QAfrPepctY2OxcWNclRC0= +github.com/alibabacloud-go/tea v1.1.18/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils v1.3.9/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils v1.4.3/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= +github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o= +github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= +github.com/alibabacloud-go/tea-xml v1.1.2 h1:oLxa7JUXm2EDFzMg+7oRsYc+kutgCVwm+bZlhhmvW5M= +github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.2.3 h1:Vmodnr52Rz1mcbwn0kzMhLRKb6soizewuKXdfZiNemU= +github.com/aliyun/credentials-go v1.2.3/go.mod h1:/KowD1cfGSLrLsH28Jr8W+xwoId0ywIy5lNzDz6O1vw= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= +github.com/apache/beam/sdks/v2 v2.0.0-20211012030016-ef4364519c94/go.mod h1:/kOom7hCyHVzAC/Z7HbZywkZZv6ywF+wb4CvgDVdcB8= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ= +github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= +github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= +github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -160,49 +484,87 @@ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250= +github.com/aws/aws-sdk-go-v2 v1.14.0/go.mod h1:ZA3Y8V0LrlWj63MQAnRHgKf/5QB//LSZCPNWlWrNGLU= github.com/aws/aws-sdk-go-v2 v1.16.13/go.mod h1:xSyvSnzh0KLs5H4HJGeIEsNYemUWdNIl0b/rP6SIsLU= -github.com/aws/aws-sdk-go-v2 v1.16.15/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= -github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk= -github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= -github.com/aws/aws-sdk-go-v2/config v1.17.4 h1:9HY1wbShqObySCHP2Z07blfrSWVX+nVxCZmUuLZKcG8= -github.com/aws/aws-sdk-go-v2/config v1.17.4/go.mod h1:ul+ru+huVpfduF9XRmGUq82T8T3K+nIFQuF6F+L+548= -github.com/aws/aws-sdk-go-v2/credentials v1.12.17 h1:htUjIJOQcvIUR0jC4eLkdis1DfaLL4EUbIKUFqh2WFA= -github.com/aws/aws-sdk-go-v2/credentials v1.12.17/go.mod h1:jd1mvJulXY7ccHvcSiJceYhv06yWIIRkJnwWEA4IX+g= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.14 h1:NZwZFtxXGOEIiCd8jWN55lexakug543CaO68bTpoLwg= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.14/go.mod h1:5CU57SyF5jZLSIw4OOll0PG83ThXwNdkRFOc0EltD/0= +github.com/aws/aws-sdk-go-v2 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw= +github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY= +github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2/config v1.5.0/go.mod h1:RWlPOAW3E3tbtNAqTwvSW54Of/yP3oiZXMI0xfUdjyA= +github.com/aws/aws-sdk-go-v2/config v1.18.3 h1:3kfBKcX3votFX84dm00U8RGA1sCCh3eRMOGzg5dCWfU= +github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU= +github.com/aws/aws-sdk-go-v2/credentials v1.3.1/go.mod h1:r0n73xwsIVagq8RsxmZbGSRQFj9As3je72C2WzUIToc= +github.com/aws/aws-sdk-go-v2/credentials v1.13.3 h1:ur+FHdp4NbVIv/49bUjBW+FE7e57HOo03ELodttmagk= +github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.3.0/go.mod h1:2LAuqPx1I6jNfaGDucWfA2zqQCYCOMCDHiCOciALyNw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 h1:E3PXZSI3F2bzyj6XxUXdTIfvp425HHhwKsFvmzBwHgs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.5/go.mod h1:2hXc8ooJqF2nAznsbJQIn+7h851/bu8GVC80OVTTqf8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.20/go.mod h1:gdZ5gRUaxThXIZyZQ8MTtgYBk2jbHgp05BO3GcD9Cwc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.22/go.mod h1:/vNv5Al0bpiF8YdX2Ov6Xy05VTiXsql94yUqJMYaj0w= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 h1:s4g/wnzMf+qepSNgTvaQQHNxyMLKSawNhKCPNy++2xY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.3.0/go.mod h1:miRSv9l093jX/t/j+mBCaLqFHo9xKYzJ7DGm1BsGoJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.14/go.mod h1:GEV9jaDPIgayiU+uevxwozcvUOjc+P4aHE2BeSjm2vE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.16/go.mod h1:62dsXI0BqTIGomDl8Hpm33dv0OntGaVblri3ZRParVQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 h1:/K482T5A3623WJgWT8w1yRAFK4RzGzEl7y39yhtn9eA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.21 h1:lpwSbLKYTuABo6SyUoC25xAmfO3/TehGS2SmD1EtOL0= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.21/go.mod h1:Q0pktZjvRZk77TBto6yAvUAi7fcse1bdcMctBDVGgBw= -github.com/aws/aws-sdk-go-v2/service/acmpca v1.18.0 h1:cnPVlhdCSBY2ee3BAjWvqGHwksQRbWJgDMB5WL2M/j0= -github.com/aws/aws-sdk-go-v2/service/acmpca v1.18.0/go.mod h1:XluaDqrmOOoqZtsCeBJ4A45ZAytjpmjr6bfmSzv/vZg= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.0 h1:9ailn+011zwUJdS8RuamANJVAyX+aoUyTaBrw0CHRdE= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.0/go.mod h1:0+6fPoY0SglgzQUs2yml7X/fup12cMlVumJufh5npRQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19/go.mod h1:6Q0546uHDp421okhmmGfbxzq2hBqbXFNpi4k+Q1JnQA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.1.1/go.mod h1:Zy8smImhTdOETZqfyn01iNOe0CNggVbPjCajyaz6Gvg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 h1:Mza+vlnZr+fPKFKRq/lKGVvM6B/8ZZmNdEopOwSQLms= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26/go.mod h1:Y2OJ+P+MC1u1VKnavT+PshiEuGPyh/7DqxoDNij4/bg= +github.com/aws/aws-sdk-go-v2/service/acmpca v1.19.0 h1:2f0kb+39miQPRp0b7Sqq06+TpxI0Nfcra41QxzJPME8= +github.com/aws/aws-sdk-go-v2/service/acmpca v1.19.0/go.mod h1:AVLIBQ9V7mcHd5uZT1+wUbBt4QaI/XcOens85Ib0W1o= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.77.0 h1:m6HYlpZlTWb9vHuuRHpWRieqPHWlS0mvQ90OJNrG/Nk= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.77.0/go.mod h1:mV0E7631M1eXdB+tlGFIw6JxfsC7Pz7+7Aw15oLVhZw= +github.com/aws/aws-sdk-go-v2/service/ecr v1.4.1/go.mod h1:FglZcyeiBqcbvyinl+n14aT/EWC7S1MIH+Gan2iizt0= +github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 h1:lY2Z2sBP+zSbJ6CvvmnFgPcgknoQ0OJV88AwVetRRFk= +github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0/go.mod h1:4zYI85WiYDhFaU1jPFVfkD7HlBcdnITDE3QxDwy4Kus= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.4.1/go.mod h1:eD5Eo4drVP2FLTw0G+SMIPWNWvQRGGTtIZR2XeAagoA= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 h1:LsqBpyRofMG6eDs6YGud6FhdGyIyXelAasPOZ6wWLro= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0/go.mod h1:IArQ3IBR00FkuraKwudKZZU32OxJfdTdwV+W5iZh3Y4= github.com/aws/aws-sdk-go-v2/service/iam v1.18.16 h1:m/WtVqEvgwDiUPIW2dtnF2hDE1O62MEflz9ClOlCXAs= github.com/aws/aws-sdk-go-v2/service/iam v1.18.16/go.mod h1:w8wndcRxwILFQAzwkUKyEDz4LDHEBSR78KRdaNjUKQA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.14/go.mod h1:8qOLjqMzY/S1kh3myDXA1yxK5eD4uN8aOJgKpgvc4OM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 h1:Jrd/oMh0PKQc6+BowB+pLEwLIgaQF29eYbe7E1Av9Ug= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= -github.com/aws/aws-sdk-go-v2/service/kms v1.18.8 h1:0YzDYm5rFuwzqwhBg94OYa2TKbdd5dUsf9+uPHwoYns= -github.com/aws/aws-sdk-go-v2/service/kms v1.18.8/go.mod h1:NjgXnn0pk5rLSWZIgtx0BCwoCugRXzKZ7cDNsl98W7U= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.0 h1:Lh1yssM4dinNZuESsXnbi+pID8hoviejLZdLmT175i8= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.0/go.mod h1:z0y2iDaghoq7uv6kndhrJCTzgVckv8Aak8kpnu2kYjs= -github.com/aws/aws-sdk-go-v2/service/sso v1.11.20 h1:3raP0UC9rvRyY4/cc4o4F3jTrNo94AYiarNUGNnq6dU= -github.com/aws/aws-sdk-go-v2/service/sso v1.11.20/go.mod h1:hPsROgDdgY/NQ1gPt7VJWG0GjSnalDC0DkkMfGEw2gc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.2 h1:/SYpdjjAtraymql+/r719OgjxezdanAQiLb/NMxDb04= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.2/go.mod h1:5cxfDYtY2mDOlmesy4yycb6lwyy1U/iAUOHKhQLKw/E= -github.com/aws/aws-sdk-go-v2/service/sts v1.16.16 h1:otZvq9r+xjPL7qU/luX2QdBamiN+oSZURRi4sAKymO8= -github.com/aws/aws-sdk-go-v2/service/sts v1.16.16/go.mod h1:Y9iBgT1w2vHtYzJEkwD6FqILjDSsvbxcW/+wIYxyse4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.1/go.mod h1:zceowr5Z1Nh2WVP8bf/3ikB41IZW59E4yIYbg+pC6mw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19/go.mod h1:02CP6iuYP+IVnBX5HULVdSAku/85eHB2Y9EsFhrkEwU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21/go.mod h1:lRToEJsn+DRA9lW4O9L9+/3hjTkUzlzyzHqn8MTds5k= +github.com/aws/aws-sdk-go-v2/service/kms v1.19.0 h1:ycl4Z01HQyprcfOFMAVwWTNaUm29qHRPZyJunDZZVXg= +github.com/aws/aws-sdk-go-v2/service/kms v1.19.0/go.mod h1:kZodDPTQjSH/qM6/OvyTfM5mms5JHB/EKYp5dhn/vI4= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0 h1:6W6BLZcXytRJsVvc2gGwxKE4wbMSlWqdxZivBP/E+ys= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0/go.mod h1:jAeo/PdIJZuDSwsvxJS94G4d6h8tStj7WXVuKwLHWU8= +github.com/aws/aws-sdk-go-v2/service/sso v1.3.1/go.mod h1:J3A3RGUvuCZjvSuZEcOpHDnzZP/sKbhDWV2T1EOzFIM= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.25 h1:GFZitO48N/7EsFDt8fMa5iYdmWqkUDDB3Eje6z3kbG0= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZoe5gvi9wjudnxvozYFJo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI= +github.com/aws/aws-sdk-go-v2/service/sts v1.6.0/go.mod h1:q7o0j7d7HrJk/vr9uUt3BVRASvcU7gYZB9PUgPiByXg= +github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 h1:60SJ4lhvn///8ygCzYy2l53bFW/Q15bVfyjyAWo6zuw= +github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4= +github.com/aws/smithy-go v1.6.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.11.0/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.13.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA= -github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795 h1:IWeCJzU+IYaO2rVEBlGPTBfe90cmGXFTLdhUFlzDGsY= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795/go.mod h1:8vJsEZ4iRqG+Vx6pKhWK6U00qcj0KC37IsfszMkY6UE= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -214,32 +576,50 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= -github.com/bytecodealliance/wasmtime-go v1.0.0 h1:9u9gqaUiaJeN5IoD1L7egD8atOnTGyJcNp8BhkL9cUU= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= +github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= +github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 h1:XlpL9EHrPOBJMLDDOf35/G4t5rGAFNNAZQ3cDcWavtc= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21/go.mod h1:Zlre/PVxuSI9y6/UV4NwGixQ48RHQDSPiUkofr6rbMU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/mxj/v2 v2.5.6 h1:Jm4VaCI/+Ug5Q57IzEoZbwx4iQFA6wkXv72juUSeK+g= +github.com/clbanning/mxj/v2 v2.5.6/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -249,55 +629,95 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= +github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= +github.com/containerd/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0= +github.com/containerd/stargz-snapshotter/estargz v0.12.1/go.mod h1:12VUuCq3qPq4y8yUW+l5w3+oXV3cx2Po3KSe/SmPGqw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g= +github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b h1:lMzA7yYThpwx7iYNpTeiQnRH6h5JSfSYMJdz+pxZOW8= +github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= -github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk= -github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/depcheck-test/depcheck-test v0.0.0-20220607135614-199033aaa936 h1:foGzavPWwtoyBvjWyKJYDYsyzy+23iBV7NKTwdk+LRY= +github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= +github.com/dgraph-io/badger/v3 v3.2103.4 h1:WE1B07YNTTJTtG9xjBcSW2wn0RJLyiV99h959RKZqM4= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.18+incompatible h1:SN84VYXTBNGn92T/QwIRPlum9zfemfitN7pbsp26WSc= -github.com/docker/docker v20.10.18+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v20.10.20+incompatible h1:lWQbHSHUFs7KraSN2jOJK7zbMS2jNCHI4mt4xUFUVQ4= +github.com/docker/cli v20.10.20+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.22+incompatible h1:6jX4yB+NtcbldT90k7vBSaWJDB3i+zkVJT9BEK8kQkk= +github.com/docker/docker v20.10.22+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= -github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -309,96 +729,189 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= +github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= -github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= -github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZUCkZIqFxsQf1o= +github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDePli4ecpK1N7gw= +github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= +github.com/fullstorydev/grpcurl v1.8.7 h1:xJWosq3BQovQ4QrdPO72OrPiWuGgEsxY8ldYsJbPrqI= +github.com/fullstorydev/grpcurl v1.8.7/go.mod h1:pVtM4qe3CMoLaIzYS8uvTuDj2jVYmXqMUkZeijnXp/E= +github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 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/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= -github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= +github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.24.2 h1:yX9HMGQbz32M87ECaAhGpJjBmErO3QLcgdZj9BzGx7c= +github.com/go-openapi/runtime v0.24.2/go.mod h1:AKurw9fNre+h3ELZfk6ILsfvPN+bvvlaU/M9q/r9hpk= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= +github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-openapi/validate v0.22.0 h1:b0QecH6VslW/TxtpKgzpO1SNG7GU2FsaqKdP1E2T50Y= +github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-rod/rod v0.112.1 h1:FuItvJ4ysJjKR2JA5UDlyLJwWZpWwA4jcNd3BoU+ioQ= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= -github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -433,14 +946,22 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/certificate-transparency-go v1.1.2 h1:4hE0GEId6NAW28dFpC+LrRGwQX5dtmXQGDbg8+/MZOM= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/certificate-transparency-go v1.1.2-0.20210422104406-9f33727a7a18/go.mod h1:6CKh9dscIRoqc2kC6YUFICHZMT9NrClyPrRVFrdw1QQ= +github.com/google/certificate-transparency-go v1.1.2-0.20210512142713-bed466244fa6/go.mod h1:aF2dp7Dh81mY8Y/zpzyXps4fQW5zQbDu2CxfpJB6NkI= +github.com/google/certificate-transparency-go v1.1.3 h1:WEb38wcTe0EuAvg7USzgklnOjjnlMaahYO3faaqnCn8= +github.com/google/certificate-transparency-go v1.1.3/go.mod h1:S9FT/VzOUzhOGG0iLrzDs+f5Ml/zm7IYY/w+IlHz01M= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= @@ -461,25 +982,41 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 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/go-containerregistry v0.12.1 h1:W1mzdNUTx4Zla4JaixCRLhORcR7G6KxE5hHl5fkPsp8= +github.com/google/go-containerregistry v0.12.1/go.mod h1:sdIK+oHQO7B93xI8UweYdl887YhuIwg9vz8BSLH3+8k= +github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= +github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= +github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= +github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= +github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= +github.com/google/go-sev-guest v0.4.1 h1:IjxtGAvzR+zSyAqMc1FWfYKCg1cwPkBly9+Xog3YMZc= +github.com/google/go-sev-guest v0.4.1/go.mod h1:UEi9uwoPbLdKGl1QHaq1G8pfCbQ4QP0swWX4J0k6r+Q= github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI= github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw= github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo= github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4= github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0= github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4= -github.com/google/go-tpm-tools v0.3.9 h1:66nkOHZtqmHXVnqonQvPDmiPRn8lcKW3FXzynJiBphg= -github.com/google/go-tpm-tools v0.3.9/go.mod h1:22JvWmHcD5w55cs+nMeqDGDxgNS15/2pDq2cLqnc3rc= +github.com/google/go-tpm-tools v0.3.10 h1:hz9EoyG4Ewa0leT3OvxlWprq14Lw0RBmfFcH9H9+Yas= +github.com/google/go-tpm-tools v0.3.10/go.mod h1:HQfQboO+M8pRtBfO5U3KMhwzfC/XC3TaMCgRfTpII8Q= github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad h1:LnpS22S8V1HqbxjveESGAazHhi6BX9SwI2Rij7qZcXQ= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/licenseclassifier v0.0.0-20210325184830-bb04aff29e72/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= +github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= +github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -489,19 +1026,33 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/trillian v1.3.14-0.20210409160123-c5ea3abd4a41/go.mod h1:1dPv0CUjNQVFEDuAUFhZql16pw/VlPgaX8qj+g5pVzQ= +github.com/google/trillian v1.3.14-0.20210511103300-67b5f349eefa/go.mod h1:s4jO3Ai4NSvxucdvqUHON0bCqJyoya32eNw6XJwsmNc= +github.com/google/trillian v1.4.1/go.mod h1:43IVCsGXxP5mZK9yFkTQdQrMQm/wryNBV2GNEdqzVz8= +github.com/google/trillian v1.5.0 h1:I5pIN18bKlXtlj1Tk919rQ3mWBU2BzNNR6JhLISGMB4= +github.com/google/trillian v1.5.0/go.mod h1:2/gAIc+G1MUcErOPc+cSwHAQHZlGy+RYHjVGnhUQ3e8= +github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -509,28 +1060,50 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 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/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/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= +github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -541,8 +1114,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.15.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo= -github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= +github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 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= @@ -551,18 +1124,19 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.0/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= -github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= -github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA= +github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/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.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/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= 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= @@ -572,10 +1146,11 @@ github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjG github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -588,27 +1163,40 @@ 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.8.1 h1:bMieWIe6dAlqAAPReZO/8zYtXaWUg/21umwqGZpEjCI= -github.com/hashicorp/vault/api v1.8.1/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= -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/vault/api v1.8.2 h1:C7OL9YtOtwQbTKI9ogB0A1wffRbCN+rH/LLCHO3d8HM= +github.com/hashicorp/vault/api v1.8.2/go.mod h1:ML8aYzBIhY5m1MD1B2Q0JV89cC85YVH4t5kBaZiyVaE= +github.com/hashicorp/vault/sdk v0.6.2 h1:LtWXUM+WheM5T8pOO/6nOTiFwnE+4y3bPztFf15Oz24= +github.com/hashicorp/vault/sdk v0.6.2/go.mod h1:KyfArJkhooyba7gYCKSq8v66QdqJmnbAxtV/OX1+JTs= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/honeycombio/beeline-go v1.10.0 h1:cUDe555oqvw8oD76BQJ8alk7FP0JZ/M/zXpNvOEDLDc= +github.com/honeycombio/libhoney-go v1.16.0 h1:kPpqoz6vbOzgp7jC6SR7SkNj7rua7rgxvznI6M3KdHc= +github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/imkira/go-observer v1.0.3 h1:l45TYAEeAB4L2xF6PR2gRLn2NE5tYhudh33MLmC7B80= github.com/imkira/go-observer v1.0.3/go.mod h1:zLzElv2cGTHufQG17IEILJMPDg32TD85fFgKyFv00wU= +github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add h1:DAh7mHiRT7wc6kKepYdCpH16ElPciMPQWJaJ7H3l/ng= +github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add/go.mod h1:DQI8vlV6h6qSY/tCOoYKtxjWrkyiNpJ3WTV/WoBllmQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -642,31 +1230,53 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08 github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.17.0/go.mod h1:Gd6RmOhtFLTu8cp/Fhq4kP195KrshxYJH3oW8AWJ1pw= +github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU= +github.com/jellydator/ttlcache/v2 v2.11.1 h1:AZGME43Eh2Vv3giG6GeqeLeFXxwxn1/qHItqWZl6U64= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= -github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w= -github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= +github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= +github.com/jhump/protoreflect v1.8.2/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= +github.com/jhump/protoreflect v1.10.3/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= +github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= +github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= +github.com/jhump/protoreflect v1.14.0 h1:MBbQK392K3u8NTLbKOCIi3XdI+y+c6yt5oMq0X3xviw= +github.com/jhump/protoreflect v1.14.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -675,21 +1285,29 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -698,71 +1316,112 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf h1:ndns1qx/5dL43g16EQkPV/i8+b3l5bYQwLeoSBe7tS8= +github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf/go.mod h1:aGkAgvWY/IUcVFfuly53REpfv5edu25oij+qHRFaraA= +github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/linkedin/goavro v2.1.0+incompatible/go.mod h1:bBCwI2eGYpUI/4820s67MElg9tdeLbINjLjiM2xZFYM= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= -github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= +github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.4 h1:qj8czE26AU4PbiaPXK5uVmMSM+V5BYsFBiM9HhGRLUA= -github.com/mitchellh/cli v1.1.4/go.mod h1:vTLESy5mRhKOs9KDp0/RATawxP1UqBmdrpVRMnpcvKQ= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3/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.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -771,55 +1430,111 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mozillazg/docker-credential-acr-helper v0.3.0 h1:DVWFZ3/O8BP6Ue3iS/Olw+G07u1hCq1EOVCDZZjCIBI= +github.com/mozillazg/docker-credential-acr-helper v0.3.0/go.mod h1:cZlu3tof523ujmLuiNUb6JsjtHcNA70u1jitrrdnuyA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= +github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= -github.com/open-policy-agent/opa v0.45.0 h1:P5nuhVRtR+e58fk3CMMbiqr6ZFyWQPNOC3otsorGsFs= -github.com/open-policy-agent/opa v0.45.0/go.mod h1:/OnsYljNEWJ6DXeFOOnoGn8CvwZGMUS4iRqzYdJvmBI= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/open-policy-agent/opa v0.47.4 h1:CTPIoAv6/UJX+BkSkqytbofWrZHyfQ/A0ESE4FSKR9A= +github.com/open-policy-agent/opa v0.47.4/go.mod h1:I5DbT677OGqfk9gvu5i54oIt0rrVf4B5pedpqDquAXo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= -github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= -github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -827,109 +1542,189 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/prometheus v2.5.0+incompatible/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= +github.com/pseudomuto/protoc-gen-doc v1.5.1/go.mod h1:XpMKYg6zkcpgfpCfQ8GcWBDRtRxOmMR5w7pz4Xo+dYM= +github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= +github.com/qur/ar v0.0.0-20130629153254-282534b91770/go.mod h1:SjlYv2m9lpV0UW6K7lDqVJwEIIvSjaHbGk7nIfY8Hxw= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 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= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= +github.com/sassoftware/go-rpmutils v0.1.1/go.mod h1:euhXULoBpvAxqrBHEyJS4Tsu3hHxUmQWNymxoJbzgUY= +github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 h1:sUNzanSKA9z/h8xXl+ZJoxIYZL0Qx306MmxqRrvUgr0= +github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74/go.mod h1:YlB8wFIZmFLZ1JllNBfSURzz52fBxbliNgYALk1UDmk= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil/v3 v3.22.9 h1:yibtJhIVEMcdw+tCTbOPiF1VcsuDeTE4utJ8Dm4c5eA= -github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A= +github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= +github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil/v3 v3.22.12 h1:oG0ns6poeUSxf78JtOsfygNWuEHYYz8hnnNg7P04TJs= +github.com/shirou/gopsutil/v3 v3.22.12/go.mod h1:Xd7P1kwZcp5VW52+9XsirIKd/BROzbb2wdX3Kqlz9uI= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sigstore/cosign v1.13.1 h1:+5oF8jisEcDw2TuXxCADC1u5//HfdnJhGbpv9Isiwu4= +github.com/sigstore/cosign v1.13.1/go.mod h1:PlfJODkovUOKsLrGI7Su57Ie/Eb/Ks7hRHw3tn5hQS4= +github.com/sigstore/fulcio v0.6.0 h1:YNfnGm9EjYPlzHiPDcIVhslYj846jkPtHQH+FTKNncw= +github.com/sigstore/fulcio v0.6.0/go.mod h1:lwxzHDYYQ0lVVWqaj68ZQNkcP847aoF7AIa7ra9rRqA= +github.com/sigstore/rekor v1.0.1 h1:rcESXSNkAPRWFYZel9rarspdvneET60F2ngNkadi89c= +github.com/sigstore/rekor v1.0.1/go.mod h1:ecTKdZWGWqE1pl3U1m1JebQJLU/hSjD9vYHOmHQ7w4g= +github.com/sigstore/sigstore v1.4.6 h1:2F1LPnQf6h1lRDCyNMoBE0WCPsA+IU5kAEAbGxG8S+U= +github.com/sigstore/sigstore v1.4.6/go.mod h1:jGHEfVTFgpfDpBz7pSY4X+Sd+g36qdAUxGufNk47k7g= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spiffe/go-spiffe/v2 v2.0.1-0.20220414143532-2ed460a8b9d3 h1:FpqM5PfWHs4Ze36HwzMpRefrv8kkmxFgtG9Qc6hL7Dc= -github.com/spiffe/go-spiffe/v2 v2.0.1-0.20220414143532-2ed460a8b9d3/go.mod h1:ifsAYiK9MOyuGYFUHUQ3K47dj+k/gd4IcWhlCyDJZEU= -github.com/spiffe/spire-api-sdk v1.2.5-0.20220608195902-84fd618158c9 h1:RmpSpUHOboDvGhxLW/32DAlV/DsvUURjojPVDMPDkwM= -github.com/spiffe/spire-api-sdk v1.2.5-0.20220608195902-84fd618158c9/go.mod h1:73BC0cOGkqRQrqoB1Djk7etxN+bE1ypmzZMkhCQs6kY= +github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= +github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/spiffe/go-spiffe/v2 v2.1.1 h1:RT9kM8MZLZIsPTH+HKQEP5yaAk3yd/VBzlINaRjXs8k= +github.com/spiffe/go-spiffe/v2 v2.1.1/go.mod h1:5qg6rpqlwIub0JAiF1UK9IMD6BpPTmvG6yfSgDBs5lg= +github.com/spiffe/spire-api-sdk v1.2.5-0.20221020001527-5895a0279944 h1:yoKYON+goNlajhkpKSfwVPB1qvmeh9MmWDyj5zc4C7o= +github.com/spiffe/spire-api-sdk v1.2.5-0.20221020001527-5895a0279944/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI= github.com/spiffe/spire-plugin-sdk v1.4.1-0.20220912221658-c42ab2d657f6 h1:QViYo6JR+v2lTMV/w9Py1mWJEXTrLn1Hb6ZsCWSVVek= github.com/spiffe/spire-plugin-sdk v1.4.1-0.20220912221658-c42ab2d657f6/go.mod h1:4KW5J6abGIAyUS8IL7Fi0NOfoWR6jA5LufKPnIdm9FE= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +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/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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= @@ -938,86 +1733,208 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= -github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= -github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= -github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= +github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 h1:iGnD/q9160NWqKZZ5vY4p0dMiYMRknzctfSkqA4nBDw= +github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613/go.mod h1:g6AnIpDSYMcphz193otpSIzN+11Rs+AAIIC6rm1enug= +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/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 h1:1i/Afw3rmaR1gF3sfVkG2X6ldkikQwA9zY380LrR5YI= +github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4/go.mod h1:vAqWV3zEs89byeFsAYoh/Q14vJTgJkHwnnRCWBBBINY= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= +github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= +github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= +github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= +github.com/transparency-dev/merkle v0.0.1 h1:T9/9gYB8uZl7VOJIhdwjALeRWlxUxSfDEysjfmx+L9E= +github.com/transparency-dev/merkle v0.0.1/go.mod h1:B8FIw5LTq6DaULoHsVFRzYIUDkl8yuSwCdZnOZGKL/A= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -github.com/uber-go/tally/v4 v4.1.3 h1:dKhkrkVSSJK0AxZCv/MmK5JrWmH+MPG3dgZCbxWk2Yc= -github.com/uber-go/tally/v4 v4.1.3/go.mod h1:aXeSTDMl4tNosyf6rdU8jlgScHyjEGGtfJ/uwCIf/vM= +github.com/uber-go/tally/v4 v4.1.4 h1:LzQyYvWQIp1gYNWU2tDNzVl04H2VchEUvMgabx/7MTI= +github.com/uber-go/tally/v4 v4.1.4/go.mod h1:aXeSTDMl4tNosyf6rdU8jlgScHyjEGGtfJ/uwCIf/vM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.7 h1:aXiFAgRugfJ27UFDsGJ9DB2FvTC73hlVXFSqq5bo9eU= +github.com/urfave/cli v1.22.7/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= +github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= +github.com/xanzy/go-gitlab v0.73.1 h1:UMagqUZLJdjss1SovIC+kJCH4k2AZWXl58gJd38Y/hI= +github.com/xanzy/go-gitlab v0.73.1/go.mod h1:d/a0vswScO7Agg1CZNz15Ic6SSvBG9vfw8egL99t4kA= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/gson v0.7.2 h1:1iWUvpi5DPvd2j59W7ifRPR9DiAZ3Ga+fmMl1mJrRbM= +github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zaffka/mongodb-boltdb-mock v0.0.0-20180816124423-49954d88fa3e/go.mod h1:GsDD1qsG+86MeeCG7ndi6Ei3iGthKL3wQ7PTFigDfNY= +github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0= github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 h1:se+XckWlVTTfwjZSsAZJ2zGPzmIMq3j7fKBCmHoB9UA= +go.etcd.io/etcd/api/v3 v3.6.0-alpha.0/go.mod h1:z13pg39zewDLZeXIKeM0xELOeFKcqjLocfwl5M820+w= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 h1:2UyRzFWbZZzgu/xzxoRukgixvafiJtGyxO+3IKUyJ6c= +go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0/go.mod h1:Vl/FkH40bHqmBFwhr8WVKtV47neyts36zl1voccRq8s= +go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 h1:9VRJ698EFIMfjOQtcjKMM7CWXOIxp9R4I8JA1mk+WT4= +go.etcd.io/etcd/client/v2 v2.306.0-alpha.0/go.mod h1:eW78BCfOzS1HJgTNzDrb2E6xV1p6kqlpLpKkz7ErzCs= +go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 h1:hHaJ8CvTPJ9iv7xPz3G0gxt3csEqJW8evgty/kYICwo= +go.etcd.io/etcd/client/v3 v3.6.0-alpha.0/go.mod h1:a9JuChoQBDnw7WclHYBYCtTOIC12Wwj+Fw0LX4TI/Gs= +go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0/go.mod h1:YPwSaBciV5G6Gpt435AasAG3ROetZsKNUzibRa/++oo= +go.etcd.io/etcd/etcdctl/v3 v3.5.4/go.mod h1:SMZep1Aj7sUmMSBCHTjkZL/Yw36Vx5Ux61fKbopbb5U= +go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 h1:3J+c4Av+pF7dBMAnxZVMrfCCMTaBz4CGJ8En3sZMNME= +go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0/go.mod h1:0ugckElRKx3OrV15/WAylLv2Ji67QxXKTh9lytkOh8s= +go.etcd.io/etcd/etcdutl/v3 v3.5.4/go.mod h1:eK9eZfI/BxDQCztpuaJ1E/ufYpMw2Y16dPX1azGWrBU= +go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 h1:DZwDkrq/z5nHxXtovJMk9fyR6Nc+pwCJt25ptlFta24= +go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0/go.mod h1:0ILo94EKC+jgp/IMfxePlfJD1OVtMVfgTQ/xM8+joOA= +go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0/go.mod h1:tV31atvwzcybuqejDoY3oaNRTtlD2l/Ot78Pc9w7DMY= +go.etcd.io/etcd/pkg/v3 v3.5.4/go.mod h1:OI+TtO+Aa3nhQSppMbwE4ld3uF1/fqqwbpfndbbrEe0= +go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 h1:cV/VsaYde/tcc2G9aHN5DQwx6CtUsWSEW4UqYzXuyyk= +go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0/go.mod h1:tXqWms0MpOJAS6L0B9nhFqZr0C/WEYzj/OtN90G8xzo= +go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0/go.mod h1:FAwse6Zlm5v4tEWZaTjmNhe17Int4Oxbu7+2r0DiD3w= +go.etcd.io/etcd/raft/v3 v3.5.4/go.mod h1:SCuunjYvZFC0fBX0vxMSPjuZmpcSk+XaAcMrD6Do03w= +go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 h1:BQ6CnNP4pIpy5rusFlTBxAacDgPXhuiHFwoTsBNsVpI= +go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0/go.mod h1:/kZdrBXlc5fUgYXfIEQ0B5sb7ejXPKbtF4jWzF1exiQ= +go.etcd.io/etcd/server/v3 v3.5.0-alpha.0/go.mod h1:tsKetYpt980ZTpzl/gb+UOJj9RkIyCb1u4wjzMg90BQ= +go.etcd.io/etcd/server/v3 v3.5.4/go.mod h1:S5/YTU15KxymM5l3T6b09sNOHPXqGYIZStpuuGbb65c= +go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 h1:BQUVqBqNFZZyrRbfydrRLzq9hYvCcRj97SsX1YwD7CA= +go.etcd.io/etcd/server/v3 v3.6.0-alpha.0/go.mod h1:3QM2rLq3B3hSXmVEvgVt3vEEbG/AumSs0Is7EgrlKzU= +go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0/go.mod h1:HnrHxjyCuZ8YDt8PYVyQQ5d1ZQfzJVEtQWllr5Vp/30= +go.etcd.io/etcd/tests/v3 v3.5.4/go.mod h1:ymig8LjkI1zqAxxMsl+nntzG21dND2hh0UQXl9BaJP8= +go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 h1:3qrZ3p/E7CxdV1kKtAU75hHOcUoXcSTwC7ELKWyzMJo= +go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0/go.mod h1:hFQkP/cTsZIXXvUv+BsGHZ3TK+76XZMi5GToYA94iac= +go.etcd.io/etcd/v3 v3.5.0-alpha.0/go.mod h1:JZ79d3LV6NUfPjUxXrpiFAYcjhT+06qqw+i28snx8To= +go.etcd.io/etcd/v3 v3.5.4/go.mod h1:c6jK4IfuWwJU26FD9SeI4cAtvlfu9Iacaxu0vRses1k= +go.etcd.io/etcd/v3 v3.6.0-alpha.0 h1:c4c3xHs9tG097KtpLfBQJSD6c70xgEZbwkoj3gF6As4= +go.etcd.io/etcd/v3 v3.6.0-alpha.0/go.mod h1:9ERPHHuSr8Ho66trD/4f3+vSeqI/hk4loUSFUwj6Zcg= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= +go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib v1.6.0/go.mod h1:FlyPNX9s4U6MCsWEc5YAK4KzKNHFDsjrDUZijJiXvy8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0 h1:xFSRQBbXF6VvYRf2lqMJXxoB72XI1K/azav8TekHHSw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0/go.mod h1:h8TWwRAhQpOd0aM5nYsRD8+flnkj+526GEIVlarH7eY= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel v1.10.0 h1:Y7DTJMR6zs1xkS/upamJYk0SxxN4C9AqRd77jmZnyY4= +go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 h1:TaB+1rQhddO1sF71MpZOZAuSPW1klK2M8XxfrBMfK7Y= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0 h1:pDDYmo0QadUPal5fwXoY1pmMpFcdyhXOmL5drCrI3vU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0/go.mod h1:Krqnjl22jUJ0HgMzw5eveuCvFDXY4nSYb4F8t5gdrag= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0 h1:KtiUEhQmj/Pa874bVYKGNVdq8NPKiacPbaRRtgXi+t4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0/go.mod h1:OfUCyyIiDvNXHWpcWgbF+MWvqPZiNa3YDEnivcnYsV0= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.10.0 h1:jZ6K7sVn04kk/3DNUdJ4mqRlGDiXAVuIG+MMENpTNdY= +go.opentelemetry.io/otel/sdk v1.10.0/go.mod h1:vO06iKzD5baltJz1zarxMCNHFpUlUiOy4s65ECtn6kE= go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/otel/trace v1.10.0 h1:npQMbR8o7mum8uF95yFbOEJffhs1sbCOfDh8zAJiH5E= +go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= 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= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -1028,34 +1945,56 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= +golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200930160638-afb6bcd081ae/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/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-20211108221036-ceb1ce70b4fa/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 h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1066,6 +2005,9 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= +golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1092,18 +2034,22 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1112,11 +2058,14 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1124,6 +2073,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -1132,34 +2082,46 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/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-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220907135653-1e95f45603a7/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/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-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1170,10 +2132,13 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= @@ -1181,22 +2146,29 @@ golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA= golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= +golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= 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= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200930132711-30421366ff76/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1204,28 +2176,35 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1237,69 +2216,90 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201005172224-997123666555/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/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-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U= -golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/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-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1308,16 +2308,23 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +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= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1326,17 +2333,23 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1344,6 +2357,7 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1361,8 +2375,9 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -1373,20 +2388,24 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1395,14 +2414,19 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T 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 h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= @@ -1421,6 +2445,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= +google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= @@ -1429,35 +2455,54 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.98.0 h1:yxZrcxXESimy6r6mdL5Q6EnZwmewDJK2dVg3g75s5Dg= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8= +google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -1491,12 +2536,21 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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-20210325141258-5636347f2b14/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs= google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210331142528-b7513248f0ba/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-20210406143921-e86de6bf7a46/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210420162539-3c870d7478d2/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210427215850-f767ed18ee4d/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1512,11 +2566,17 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= @@ -1528,7 +2588,10 @@ google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= 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= @@ -1536,17 +2599,40 @@ google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljW google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220902135211-223410557253/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 h1:mmbq5q8M1t7dhkLw320YK4PsOXm6jdnUAkErImaIqOg= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 h1:AGXp12e/9rItf6/4QymU7WsAUwCf+ICW75cuR91nJIc= +google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6/go.mod h1:1dOng4TWOomJrDGhpXjfCD35wQC6jnC7HpRmOFRqEV0= 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= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1556,6 +2642,7 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= @@ -1569,6 +2656,7 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= 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= @@ -1576,9 +2664,12 @@ google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0 h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= google.golang.org/grpc/examples v0.0.0-20201130180447-c456688b1860/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= 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= @@ -1598,27 +2689,41 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw 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/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= +gopkg.in/cheggaaa/pb.v1 v1.0.28/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= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/linkedin/goavro.v1 v1.0.5/go.mod h1:Aw5GdAbizjOEl0kAMHV9iHmA8reZzW/OKuJAl4Hb9F0= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1630,14 +2735,14 @@ 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-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +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= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1645,50 +2750,39 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= -k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= -k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= -k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= -k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= -k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= -k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= -k8s.io/apiserver v0.23.3/go.mod h1:3HhsTmC+Pn+Jctw+Ow0LHA4dQ4oXrQ4XJDzrVDG64T4= -k8s.io/client-go v0.23.3/go.mod h1:47oMd+YvAOqZM7pcQ6neJtBiFH7alOyfunYN48VsmwE= -k8s.io/client-go v0.25.2 h1:SUPp9p5CwM0yXGQrwYurw9LWz+YtMwhWd0GqOsSiefo= -k8s.io/client-go v0.25.2/go.mod h1:i7cNU7N+yGQmJkewcRD2+Vuj4iz7b30kI8OcL3horQ4= -k8s.io/code-generator v0.23.3/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= -k8s.io/component-base v0.23.3/go.mod h1:1Smc4C60rWG7d3HjSYpIwEbySQ3YWg0uzH5a2AtaTLg= -k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= -k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= -k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-aggregator v0.23.3 h1:9IP+D+YzIbGor/TArN3pYf9Thj19wYhzLRGRrFaKFSs= -k8s.io/kube-aggregator v0.23.3/go.mod h1:pt5QJ3QaIdhZzNlUvN5wndbM0LNT4BvhszGkzy2QdFo= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= -k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= +k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= +k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= +k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= +k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= +k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= +k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= +k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= +k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs= +k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-aggregator v0.26.0 h1:XF/Q5FwdLmCsK1RKGFNWfIo/b+r63sXOu+KKcaIFa/M= +k8s.io/kube-aggregator v0.26.0/go.mod h1:QUGAvubVFZ43JiT2gMm6f15FvFkyJcZeDcV1nIbmfgk= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= +k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= -sigs.k8s.io/controller-runtime v0.13.0 h1:iqa5RNciy7ADWnIc8QxCbOX5FEKVR3uxVxKHRMc2WIQ= -sigs.k8s.io/controller-runtime v0.13.0/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM= +sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU= sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/release-utils v0.7.3 h1:6pS8x6c5RmdUgR9qcg1LO6hjUzuE4Yo9TGZ3DemrZdM= +sigs.k8s.io/release-utils v0.7.3/go.mod h1:n0mVez/1PZYZaZUTJmxewxH3RJ/Lf7JUDh7TG1CASOE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/pkg/agent/attestor/node/node.go b/pkg/agent/attestor/node/node.go index 2d5e78c045..be854945d8 100644 --- a/pkg/agent/attestor/node/node.go +++ b/pkg/agent/attestor/node/node.go @@ -259,6 +259,7 @@ func (a *attestor) serverConn(ctx context.Context, bundle *bundleutil.Bundle) (* return grpc.DialContext(ctx, a.c.ServerAddress, grpc.WithDefaultServiceConfig(roundRobinServiceConfig), + grpc.WithDisableServiceConfig(), grpc.FailOnNonTempDialError(true), grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), grpc.WithReturnConnectionError(), diff --git a/pkg/agent/client/dial.go b/pkg/agent/client/dial.go index 74c603fe71..ee6625391a 100644 --- a/pkg/agent/client/dial.go +++ b/pkg/agent/client/dial.go @@ -65,6 +65,7 @@ func DialServer(ctx context.Context, config DialServerConfig) (*grpc.ClientConn, } client, err := config.dialContext(ctx, config.Address, grpc.WithDefaultServiceConfig(roundRobinServiceConfig), + grpc.WithDisableServiceConfig(), grpc.FailOnNonTempDialError(true), grpc.WithBlock(), grpc.WithReturnConnectionError(), diff --git a/pkg/agent/manager/cache/cache_test.go b/pkg/agent/manager/cache/cache_test.go index 8f8372842e..037af7c070 100644 --- a/pkg/agent/manager/cache/cache_test.go +++ b/pkg/agent/manager/cache/cache_test.go @@ -17,14 +17,15 @@ import ( ) var ( - trustDomain1 = spiffeid.RequireTrustDomainFromString("domain.test") - trustDomain2 = spiffeid.RequireTrustDomainFromString("otherdomain.test") - bundleV1 = bundleutil.BundleFromRootCA(trustDomain1, &x509.Certificate{Raw: []byte{1}}) - bundleV2 = bundleutil.BundleFromRootCA(trustDomain1, &x509.Certificate{Raw: []byte{2}}) - bundleV3 = bundleutil.BundleFromRootCA(trustDomain1, &x509.Certificate{Raw: []byte{3}}) - otherBundleV1 = bundleutil.BundleFromRootCA(trustDomain2, &x509.Certificate{Raw: []byte{4}}) - otherBundleV2 = bundleutil.BundleFromRootCA(trustDomain2, &x509.Certificate{Raw: []byte{5}}) - defaultTTL = int32(600) + trustDomain1 = spiffeid.RequireTrustDomainFromString("domain.test") + trustDomain2 = spiffeid.RequireTrustDomainFromString("otherdomain.test") + bundleV1 = bundleutil.BundleFromRootCA(trustDomain1, &x509.Certificate{Raw: []byte{1}}) + bundleV2 = bundleutil.BundleFromRootCA(trustDomain1, &x509.Certificate{Raw: []byte{2}}) + bundleV3 = bundleutil.BundleFromRootCA(trustDomain1, &x509.Certificate{Raw: []byte{3}}) + otherBundleV1 = bundleutil.BundleFromRootCA(trustDomain2, &x509.Certificate{Raw: []byte{4}}) + otherBundleV2 = bundleutil.BundleFromRootCA(trustDomain2, &x509.Certificate{Raw: []byte{5}}) + defaultX509SVIDTTL = int32(700) + defaultJwtSVIDTTL = int32(800) ) func TestFetchWorkloadUpdate(t *testing.T) { @@ -487,7 +488,7 @@ func TestCheckSVIDCallback(t *testing.T) { return false }) - foo := makeRegistrationEntryWithTTL("FOO", 60) + foo := makeRegistrationEntryWithTTL("FOO", 70, 80) // called once for FOO with no SVID callCount := 0 @@ -536,7 +537,7 @@ func TestCheckSVIDCallback(t *testing.T) { func TestGetStaleEntries(t *testing.T) { cache := newTestCache() - foo := makeRegistrationEntryWithTTL("FOO", 60) + foo := makeRegistrationEntryWithTTL("FOO", 70, 80) // Create entry but don't mark it stale cache.UpdateEntries(&UpdateEntries{ @@ -787,21 +788,23 @@ func makeX509SVIDs(entries ...*common.RegistrationEntry) map[string]*X509SVID { func makeRegistrationEntry(id string, selectors ...string) *common.RegistrationEntry { return &common.RegistrationEntry{ - EntryId: id, - SpiffeId: "spiffe://domain.test/" + id, - Selectors: makeSelectors(selectors...), - DnsNames: []string{fmt.Sprintf("name-%s", id)}, - Ttl: defaultTTL, + EntryId: id, + SpiffeId: "spiffe://domain.test/" + id, + Selectors: makeSelectors(selectors...), + DnsNames: []string{fmt.Sprintf("name-%s", id)}, + X509SvidTtl: defaultX509SVIDTTL, + JwtSvidTtl: defaultJwtSVIDTTL, } } -func makeRegistrationEntryWithTTL(id string, ttl int32, selectors ...string) *common.RegistrationEntry { +func makeRegistrationEntryWithTTL(id string, x509SVIDTTL int32, jwtSVIDTTL int32, selectors ...string) *common.RegistrationEntry { return &common.RegistrationEntry{ - EntryId: id, - SpiffeId: "spiffe://domain.test/" + id, - Selectors: makeSelectors(selectors...), - DnsNames: []string{fmt.Sprintf("name-%s", id)}, - Ttl: ttl, + EntryId: id, + SpiffeId: "spiffe://domain.test/" + id, + Selectors: makeSelectors(selectors...), + DnsNames: []string{fmt.Sprintf("name-%s", id)}, + X509SvidTtl: x509SVIDTTL, + JwtSvidTtl: jwtSVIDTTL, } } diff --git a/pkg/agent/manager/cache/lru_cache_test.go b/pkg/agent/manager/cache/lru_cache_test.go index 8fd5ea2bce..520717b811 100644 --- a/pkg/agent/manager/cache/lru_cache_test.go +++ b/pkg/agent/manager/cache/lru_cache_test.go @@ -471,7 +471,7 @@ func TestLRUCacheCheckSVIDCallback(t *testing.T) { return false }) - foo := makeRegistrationEntryWithTTL("FOO", 60) + foo := makeRegistrationEntryWithTTL("FOO", 70, 80) cache.UpdateEntries(&UpdateEntries{ Bundles: makeBundles(bundleV2), @@ -509,7 +509,7 @@ func TestLRUCacheCheckSVIDCallback(t *testing.T) { func TestLRUCacheGetStaleEntries(t *testing.T) { cache := newTestLRUCache() - bar := makeRegistrationEntryWithTTL("BAR", 120, "B") + bar := makeRegistrationEntryWithTTL("BAR", 130, 140, "B") // Create entry but don't mark it stale from checkSVID method; // it will be marked stale cause it does not have SVID cached diff --git a/pkg/agent/manager/storecache/cache_test.go b/pkg/agent/manager/storecache/cache_test.go index cf47b6114a..3a5e850789 100644 --- a/pkg/agent/manager/storecache/cache_test.go +++ b/pkg/agent/manager/storecache/cache_test.go @@ -352,7 +352,8 @@ func TestUpdateEntries(t *testing.T) { setUpdate: func(update cache.UpdateEntries) *cache.UpdateEntries { updatedEntry := createTestEntry() updatedEntry.RevisionNumber = 3 - updatedEntry.Ttl = 1234 + updatedEntry.X509SvidTtl = 2345 + updatedEntry.JwtSvidTtl = 3456 update.RegistrationEntries["foh"] = updatedEntry @@ -375,7 +376,8 @@ func TestUpdateEntries(t *testing.T) { SpiffeId: fohID.String(), StoreSvid: true, RevisionNumber: 3, - Ttl: 1234, + X509SvidTtl: 2345, + JwtSvidTtl: 3456, }, Revision: 2, }, diff --git a/pkg/agent/plugin/keymanager/base/keymanagerbase.go b/pkg/agent/plugin/keymanager/base/keymanagerbase.go index cb845803c3..93d66299e3 100644 --- a/pkg/agent/plugin/keymanager/base/keymanagerbase.go +++ b/pkg/agent/plugin/keymanager/base/keymanagerbase.go @@ -26,20 +26,27 @@ type KeyEntry struct { *keymanagerv1.PublicKey } -// Funcs is a collection of optional callbacks. Default implementations will be +// Config is a collection of optional callbacks. Default implementations will be // used when not provided. -type Funcs struct { - WriteEntries func(ctx context.Context, allEntries []*KeyEntry, newEntry *KeyEntry) error - GenerateRSA2048Key func() (*rsa.PrivateKey, error) - GenerateRSA4096Key func() (*rsa.PrivateKey, error) - GenerateEC256Key func() (*ecdsa.PrivateKey, error) - GenerateEC384Key func() (*ecdsa.PrivateKey, error) +type Config struct { + // Generator is an optional key generator. + Generator Generator + + // WriteEntries is an optional callback used to persist key entries + WriteEntries func(ctx context.Context, allEntries []*KeyEntry, newEntry *KeyEntry) error +} + +type Generator interface { + GenerateRSA2048Key() (*rsa.PrivateKey, error) + GenerateRSA4096Key() (*rsa.PrivateKey, error) + GenerateEC256Key() (*ecdsa.PrivateKey, error) + GenerateEC384Key() (*ecdsa.PrivateKey, error) } // Base is the base KeyManager implementation type Base struct { keymanagerv1.UnsafeKeyManagerServer - funcs Funcs + config Config mu sync.RWMutex entries map[string]*KeyEntry @@ -47,21 +54,12 @@ type Base struct { // New creates a new base key manager using the provided Funcs. Default // implementations are provided for any that aren't set. -func New(funcs Funcs) *Base { - if funcs.GenerateRSA2048Key == nil { - funcs.GenerateRSA2048Key = generateRSA2048Key - } - if funcs.GenerateRSA4096Key == nil { - funcs.GenerateRSA4096Key = generateRSA4096Key - } - if funcs.GenerateEC256Key == nil { - funcs.GenerateEC256Key = generateEC256Key - } - if funcs.GenerateEC384Key == nil { - funcs.GenerateEC384Key = generateEC384Key +func New(config Config) *Base { + if config.Generator == nil { + config.Generator = defaultGenerator{} } return &Base{ - funcs: funcs, + config: config, entries: make(map[string]*KeyEntry), } } @@ -142,8 +140,8 @@ func (m *Base) generateKey(ctx context.Context, req *keymanagerv1.GenerateKeyReq m.entries[req.KeyId] = newEntry - if m.funcs.WriteEntries != nil { - if err := m.funcs.WriteEntries(ctx, entriesSliceFromMap(m.entries), newEntry); err != nil { + if m.config.WriteEntries != nil { + if err := m.config.WriteEntries(ctx, entriesSliceFromMap(m.entries), newEntry); err != nil { if hasEntry { m.entries[req.KeyId] = oldEntry } else { @@ -217,13 +215,13 @@ func (m *Base) generateKeyEntry(keyID string, keyType keymanagerv1.KeyType) (e * var privateKey crypto.Signer switch keyType { case keymanagerv1.KeyType_EC_P256: - privateKey, err = m.funcs.GenerateEC256Key() + privateKey, err = m.config.Generator.GenerateEC256Key() case keymanagerv1.KeyType_EC_P384: - privateKey, err = m.funcs.GenerateEC384Key() + privateKey, err = m.config.Generator.GenerateEC384Key() case keymanagerv1.KeyType_RSA_2048: - privateKey, err = m.funcs.GenerateRSA2048Key() + privateKey, err = m.config.Generator.GenerateRSA2048Key() case keymanagerv1.KeyType_RSA_4096: - privateKey, err = m.funcs.GenerateRSA4096Key() + privateKey, err = m.config.Generator.GenerateRSA4096Key() default: return nil, status.Errorf(codes.InvalidArgument, "unable to generate key %q for unknown key type %q", keyID, keyType) } @@ -299,19 +297,21 @@ func ecdsaKeyType(privateKey *ecdsa.PrivateKey) (keymanagerv1.KeyType, error) { } } -func generateRSA2048Key() (*rsa.PrivateKey, error) { +type defaultGenerator struct{} + +func (defaultGenerator) GenerateRSA2048Key() (*rsa.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, 2048) } -func generateRSA4096Key() (*rsa.PrivateKey, error) { +func (defaultGenerator) GenerateRSA4096Key() (*rsa.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, 4096) } -func generateEC256Key() (*ecdsa.PrivateKey, error) { +func (defaultGenerator) GenerateEC256Key() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) } -func generateEC384Key() (*ecdsa.PrivateKey, error) { +func (defaultGenerator) GenerateEC384Key() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) } diff --git a/pkg/agent/plugin/keymanager/base/keymanagerbase_test.go b/pkg/agent/plugin/keymanager/base/keymanagerbase_test.go new file mode 100644 index 0000000000..623717f3f1 --- /dev/null +++ b/pkg/agent/plugin/keymanager/base/keymanagerbase_test.go @@ -0,0 +1,14 @@ +package keymanagerbase + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSetsConfigDefaults(t *testing.T) { + // This test makes sure that we wire up the default functions + b := New(Config{}) + assert.Equal(t, defaultGenerator{}, b.config.Generator) + assert.Nil(t, b.config.WriteEntries) +} diff --git a/pkg/agent/plugin/keymanager/disk/disk.go b/pkg/agent/plugin/keymanager/disk/disk.go index f7d485d246..8a91014ad8 100644 --- a/pkg/agent/plugin/keymanager/disk/disk.go +++ b/pkg/agent/plugin/keymanager/disk/disk.go @@ -19,11 +19,17 @@ import ( "google.golang.org/grpc/status" ) +type Generator = keymanagerbase.Generator + func BuiltIn() catalog.BuiltIn { - return builtin(New()) + return asBuiltIn(newKeyManager(nil)) +} + +func TestBuiltIn(generator Generator) catalog.BuiltIn { + return asBuiltIn(newKeyManager(generator)) } -func builtin(p *KeyManager) catalog.BuiltIn { +func asBuiltIn(p *KeyManager) catalog.BuiltIn { return catalog.MakeBuiltIn("disk", keymanagerv1.KeyManagerPluginServer(p), configv1.ConfigServiceServer(p)) @@ -43,9 +49,10 @@ type KeyManager struct { config *configuration } -func New() *KeyManager { +func newKeyManager(generator Generator) *KeyManager { m := &KeyManager{} - m.Base = keymanagerbase.New(keymanagerbase.Funcs{ + m.Base = keymanagerbase.New(keymanagerbase.Config{ + Generator: generator, WriteEntries: m.writeEntries, }) return m @@ -160,7 +167,7 @@ func writeEntries(path string, entries []*keymanagerbase.KeyEntry) error { return status.Errorf(codes.Internal, "unable to marshal entries: %v", err) } - if err := diskutil.AtomicWriteFile(path, jsonBytes, 0600); err != nil { + if err := diskutil.AtomicWritePrivateFile(path, jsonBytes); err != nil { return status.Errorf(codes.Internal, "unable to write entries: %v", err) } diff --git a/pkg/agent/plugin/keymanager/disk/disk_test.go b/pkg/agent/plugin/keymanager/disk/disk_test.go index ec14a091dc..6b0f9a5ec5 100644 --- a/pkg/agent/plugin/keymanager/disk/disk_test.go +++ b/pkg/agent/plugin/keymanager/disk/disk_test.go @@ -83,7 +83,8 @@ func TestGenerateKeyPersistence(t *testing.T) { func loadPlugin(t *testing.T, configFmt string, configArgs ...interface{}) (keymanager.KeyManager, error) { km := new(keymanager.V1) var configErr error - plugintest.Load(t, disk.BuiltIn(), km, + + plugintest.Load(t, disk.TestBuiltIn(keymanagertest.NewGenerator()), km, plugintest.Configuref(configFmt, configArgs...), plugintest.CaptureConfigureError(&configErr), ) diff --git a/pkg/agent/plugin/keymanager/memory/memory.go b/pkg/agent/plugin/keymanager/memory/memory.go index 50b524fdd1..b1384f5193 100644 --- a/pkg/agent/plugin/keymanager/memory/memory.go +++ b/pkg/agent/plugin/keymanager/memory/memory.go @@ -6,11 +6,17 @@ import ( "github.com/spiffe/spire/pkg/common/catalog" ) +type Generator = keymanagerbase.Generator + func BuiltIn() catalog.BuiltIn { - return builtin(New()) + return asBuiltIn(newKeyManager(nil)) +} + +func TestBuiltIn(generator Generator) catalog.BuiltIn { + return asBuiltIn(newKeyManager(generator)) } -func builtin(p *KeyManager) catalog.BuiltIn { +func asBuiltIn(p *KeyManager) catalog.BuiltIn { return catalog.MakeBuiltIn("memory", keymanagerv1.KeyManagerPluginServer(p)) } @@ -18,8 +24,10 @@ type KeyManager struct { *keymanagerbase.Base } -func New() *KeyManager { +func newKeyManager(generator Generator) *KeyManager { return &KeyManager{ - Base: keymanagerbase.New(keymanagerbase.Funcs{}), + Base: keymanagerbase.New(keymanagerbase.Config{ + Generator: generator, + }), } } diff --git a/pkg/agent/plugin/keymanager/memory/memory_test.go b/pkg/agent/plugin/keymanager/memory/memory_test.go index 46cc5b0b4a..bf90bee1b7 100644 --- a/pkg/agent/plugin/keymanager/memory/memory_test.go +++ b/pkg/agent/plugin/keymanager/memory/memory_test.go @@ -13,7 +13,7 @@ func TestKeyManagerContract(t *testing.T) { keymanagertest.Test(t, keymanagertest.Config{ Create: func(t *testing.T) keymanager.KeyManager { km := new(keymanager.V1) - plugintest.Load(t, memory.BuiltIn(), km) + plugintest.Load(t, memory.TestBuiltIn(keymanagertest.NewGenerator()), km) return km }, }) diff --git a/pkg/agent/plugin/keymanager/test/keymanagertest.go b/pkg/agent/plugin/keymanager/test/keymanagertest.go index dfc2e5866f..01328e743a 100644 --- a/pkg/agent/plugin/keymanager/test/keymanagertest.go +++ b/pkg/agent/plugin/keymanager/test/keymanagertest.go @@ -10,11 +10,15 @@ import ( "crypto/sha256" "crypto/x509" "math/big" + "os" + "strconv" "testing" "github.com/spiffe/spire/pkg/agent/plugin/keymanager" + keymanagerbase "github.com/spiffe/spire/pkg/agent/plugin/keymanager/base" "github.com/spiffe/spire/pkg/common/plugin" "github.com/spiffe/spire/test/spiretest" + "github.com/spiffe/spire/test/testkey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -48,6 +52,13 @@ var ( } ) +func NewGenerator() keymanagerbase.Generator { + if nightly, err := strconv.ParseBool(os.Getenv("NIGHTLY")); err == nil && nightly { + return nil + } + return &testkey.Generator{} +} + type CreateFunc = func(t *testing.T) keymanager.KeyManager type Config struct { diff --git a/pkg/agent/plugin/nodeattestor/tpmdevid/devid_test.go b/pkg/agent/plugin/nodeattestor/tpmdevid/devid_test.go index 434e54926f..152b6f65fd 100644 --- a/pkg/agent/plugin/nodeattestor/tpmdevid/devid_test.go +++ b/pkg/agent/plugin/nodeattestor/tpmdevid/devid_test.go @@ -1,3 +1,6 @@ +//go:build !darwin +// +build !darwin + package tpmdevid_test import ( diff --git a/pkg/agent/plugin/nodeattestor/tpmdevid/tpmutil/session_test.go b/pkg/agent/plugin/nodeattestor/tpmdevid/tpmutil/session_test.go index 847f6586b7..2b276e6235 100644 --- a/pkg/agent/plugin/nodeattestor/tpmdevid/tpmutil/session_test.go +++ b/pkg/agent/plugin/nodeattestor/tpmdevid/tpmutil/session_test.go @@ -1,3 +1,6 @@ +//go:build !darwin +// +build !darwin + package tpmutil_test import ( diff --git a/pkg/agent/plugin/svidstore/gcpsecretmanager/client.go b/pkg/agent/plugin/svidstore/gcpsecretmanager/client.go index 414d09f929..c08097e6ec 100644 --- a/pkg/agent/plugin/svidstore/gcpsecretmanager/client.go +++ b/pkg/agent/plugin/svidstore/gcpsecretmanager/client.go @@ -3,11 +3,11 @@ package gcpsecretmanager import ( "context" + "cloud.google.com/go/iam/apiv1/iampb" secretmanager "cloud.google.com/go/secretmanager/apiv1" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" gax "github.com/googleapis/gax-go/v2" "google.golang.org/api/option" - secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" - iampb "google.golang.org/genproto/googleapis/iam/v1" ) type secretManagerClient interface { diff --git a/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud.go b/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud.go index a994eb8256..04f2735e82 100644 --- a/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud.go +++ b/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud.go @@ -9,14 +9,14 @@ import ( "strings" "sync" + "cloud.google.com/go/iam/apiv1/iampb" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "github.com/hashicorp/go-hclog" "github.com/hashicorp/hcl" svidstorev1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/agent/svidstore/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire/pkg/agent/plugin/svidstore" "github.com/spiffe/spire/pkg/common/catalog" - secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" - "google.golang.org/genproto/googleapis/iam/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -225,7 +225,7 @@ func (p *SecretManagerPlugin) shouldSetPolicy(ctx context.Context, secretName st if !secretFound { return true, nil } - policy, err := p.secretManagerClient.GetIamPolicy(ctx, &iam.GetIamPolicyRequest{ + policy, err := p.secretManagerClient.GetIamPolicy(ctx, &iampb.GetIamPolicyRequest{ Resource: secretName, }) if err != nil { @@ -251,10 +251,10 @@ func (p *SecretManagerPlugin) shouldSetPolicy(ctx context.Context, secretName st func (p *SecretManagerPlugin) setIamPolicy(ctx context.Context, secretName string, opt *secretOptions) error { // Create a policy without conditions and a single binding - resp, err := p.secretManagerClient.SetIamPolicy(ctx, &iam.SetIamPolicyRequest{ + resp, err := p.secretManagerClient.SetIamPolicy(ctx, &iampb.SetIamPolicyRequest{ Resource: opt.secretName(), - Policy: &iam.Policy{ - Bindings: []*iam.Binding{ + Policy: &iampb.Policy{ + Bindings: []*iampb.Binding{ { Role: opt.roleName, Members: []string{opt.serviceAccount}, diff --git a/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud_test.go b/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud_test.go index 911193add5..fc8674e190 100644 --- a/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud_test.go +++ b/pkg/agent/plugin/svidstore/gcpsecretmanager/gcloud_test.go @@ -11,6 +11,8 @@ import ( "testing" "time" + "cloud.google.com/go/iam/apiv1/iampb" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" gax "github.com/googleapis/gax-go/v2" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/spire/pkg/agent/plugin/svidstore" @@ -20,8 +22,6 @@ import ( "github.com/spiffe/spire/test/spiretest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" - iampb "google.golang.org/genproto/googleapis/iam/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/pkg/agent/plugin/workloadattestor/k8s/k8s.go b/pkg/agent/plugin/workloadattestor/k8s/k8s.go index 27952a7bfa..58b9b6c5d2 100644 --- a/pkg/agent/plugin/workloadattestor/k8s/k8s.go +++ b/pkg/agent/plugin/workloadattestor/k8s/k8s.go @@ -40,6 +40,7 @@ const ( defaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" //nolint: gosec // false positive defaultNodeNameEnv = "MY_NODE_NAME" defaultReloadInterval = time.Minute + maximumAmountCache = 10 ) func BuiltIn() catalog.BuiltIn { @@ -119,6 +120,29 @@ type HCLConfig struct { // but the container may not be in a ready state at the time of attestation // (e.g. when a postStart hook has yet to complete). DisableContainerSelectors bool `hcl:"disable_container_selectors"` + + // Experimental enables experimental features. + Experimental *ExperimentalK8SConfig `hcl:"experimental,omitempty"` +} + +type ExperimentalK8SConfig struct { + // Sigstore contains sigstore specific configs. + Sigstore *SigstoreHCLConfig `hcl:"sigstore,omitempty"` +} + +// SigstoreHCLConfig holds the sigstore configuration parsed from HCL +type SigstoreHCLConfig struct { + // EnforceSCT is the parameter to be set as false in case of a private deployment not using the public CT + EnforceSCT *bool `hcl:"enforce_sct, omitempty"` + + // RekorURL is the URL for the rekor server to use to verify signatures and public keys + RekorURL *string `hcl:"rekor_url,omitempty"` + + // SkippedImages is a list of images that should skip sigstore verification + SkippedImages []string `hcl:"skip_signature_verification_image_list"` + + // AllowedSubjects is a list of subjects that should be allowed after verification + AllowedSubjects map[string][]string `hcl:"allowed_subjects_list"` } // k8sConfig holds the configuration distilled from HCL @@ -142,6 +166,8 @@ type k8sConfig struct { } type ContainerHelper interface { + Configure(config *HCLConfig, log hclog.Logger) error + GetOSSelectors(ctx context.Context, log hclog.Logger, containerStatus *corev1.ContainerStatus) ([]string, error) GetPodUIDAndContainerID(pID int32, log hclog.Logger) (types.UID, string, error) } @@ -222,7 +248,16 @@ func (p *Plugin) Attest(ctx context.Context, req *workloadattestorv1.AttestReque selectorValues = append(selectorValues, getSelectorValuesFromPodInfo(&item)...) if !config.DisableContainerSelectors { selectorValues = append(selectorValues, getSelectorValuesFromWorkloadContainerStatus(containerStatus)...) + + osSelector, err := p.c.GetOSSelectors(ctx, log, containerStatus) + switch { + case err != nil: + return nil, err + case len(osSelector) > 0: + selectorValues = append(selectorValues, osSelector...) + } } + case podKnown && config.DisableContainerSelectors: // The workload container was not found (i.e. not ready yet?) // but the pod is known. If container selectors have been @@ -305,8 +340,8 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return nil, status.Error(codes.InvalidArgument, "cannot use both the read-only and secure port") } - containerHelper, err := createHelper(p) - if err != nil { + containerHelper := createHelper(p) + if err := containerHelper.Configure(config, p.log); err != nil { return nil, err } @@ -340,6 +375,7 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) ReloadInterval: reloadInterval, DisableContainerSelectors: config.DisableContainerSelectors, } + if err := p.reloadKubeletClient(c); err != nil { return nil, err } diff --git a/pkg/agent/plugin/workloadattestor/k8s/k8s_posix.go b/pkg/agent/plugin/workloadattestor/k8s/k8s_posix.go index a81555e8ba..1264f750fd 100644 --- a/pkg/agent/plugin/workloadattestor/k8s/k8s_posix.go +++ b/pkg/agent/plugin/workloadattestor/k8s/k8s_posix.go @@ -4,6 +4,7 @@ package k8s import ( + "context" "log" "regexp" "strings" @@ -11,8 +12,11 @@ import ( "github.com/hashicorp/go-hclog" "github.com/spiffe/spire/pkg/agent/common/cgroups" + "github.com/spiffe/spire/pkg/agent/plugin/workloadattestor/k8s/sigstore" + "github.com/spiffe/spire/pkg/common/telemetry" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -24,14 +28,46 @@ func (p *Plugin) defaultTokenPath() string { return defaultTokenPath } -func createHelper(c *Plugin) (ContainerHelper, error) { +func createHelper(c *Plugin) ContainerHelper { return &containerHelper{ fs: c.fs, - }, nil + } } type containerHelper struct { - fs cgroups.FileSystem + fs cgroups.FileSystem + sigstoreClient sigstore.Sigstore +} + +func (h *containerHelper) Configure(config *HCLConfig, log hclog.Logger) error { + // set experimental flags + if config.Experimental != nil && config.Experimental.Sigstore != nil { + if h.sigstoreClient == nil { + newcache := sigstore.NewCache(maximumAmountCache) + h.sigstoreClient = sigstore.New(newcache, nil) + } + + if err := configureSigstoreClient(h.sigstoreClient, config.Experimental.Sigstore, log); err != nil { + return err + } + } + + return nil +} + +func (h *containerHelper) GetOSSelectors(ctx context.Context, log hclog.Logger, containerStatus *corev1.ContainerStatus) ([]string, error) { + var selectors []string + if h.sigstoreClient != nil { + log.Debug("Attemping to get signature info for container", telemetry.ContainerName, containerStatus.Name) + sigstoreSelectors, err := h.sigstoreClient.AttestContainerSignatures(ctx, containerStatus) + if err != nil { + log.Error("Error retrieving signature payload", "error", err) + return nil, status.Errorf(codes.Internal, "error retrieving signature payload: %v", err) + } + selectors = append(selectors, sigstoreSelectors...) + } + + return selectors, nil } func (h *containerHelper) GetPodUIDAndContainerID(pID int32, _ hclog.Logger) (types.UID, string, error) { @@ -70,7 +106,7 @@ func getPodUIDAndContainerIDFromCGroups(cgroups []cgroups.Cgroup) (types.UID, st return podUID, containerID, nil } -// regexes listed here have to exlusively match a cgroup path +// regexes listed here have to exclusively match a cgroup path // the regexes must include two named groups "poduid" and "containerid" // if the regex needs to exclude certain substrings, the "mustnotmatch" group can be used var cgroupREs = []*regexp.Regexp{ @@ -175,3 +211,34 @@ func canonicalizePodUID(uid string) types.UID { return r }, uid)) } + +func configureSigstoreClient(client sigstore.Sigstore, c *SigstoreHCLConfig, log hclog.Logger) error { + // Rekor URL is required + if c.RekorURL == nil { + return status.Errorf(codes.InvalidArgument, "missing Rekor URL") + } + if err := client.SetRekorURL(*c.RekorURL); err != nil { + return status.Errorf(codes.InvalidArgument, "failed to set Rekor URL: %v", err) + } + + // Configure sigstore settings + enforceSCT := true + if c.EnforceSCT != nil { + enforceSCT = *c.EnforceSCT + } + + client.SetEnforceSCT(enforceSCT) + + client.ClearSkipList() + if c.SkippedImages != nil { + client.AddSkippedImages(c.SkippedImages) + } + client.SetLogger(log) + client.ClearAllowedSubjects() + for issuer, subjects := range c.AllowedSubjects { + for _, subject := range subjects { + client.AddAllowedSubject(issuer, subject) + } + } + return nil +} diff --git a/pkg/agent/plugin/workloadattestor/k8s/k8s_posix_test.go b/pkg/agent/plugin/workloadattestor/k8s/k8s_posix_test.go index e93496a017..aeda010b79 100644 --- a/pkg/agent/plugin/workloadattestor/k8s/k8s_posix_test.go +++ b/pkg/agent/plugin/workloadattestor/k8s/k8s_posix_test.go @@ -4,18 +4,26 @@ package k8s import ( + "bytes" "context" + "errors" "fmt" "os" "path/filepath" "testing" + "github.com/hashicorp/go-hclog" + "github.com/sigstore/cosign/pkg/oci" "github.com/spiffe/spire/pkg/agent/common/cgroups" "github.com/spiffe/spire/pkg/agent/plugin/workloadattestor" + "github.com/spiffe/spire/pkg/agent/plugin/workloadattestor/k8s/sigstore" "github.com/spiffe/spire/proto/spire/common" + "github.com/spiffe/spire/test/plugintest" "github.com/spiffe/spire/test/spiretest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -95,6 +103,51 @@ var ( {Type: "k8s", Value: "pod-uid:d488cae9-b2a0-11e7-9350-020968147796"}, {Type: "k8s", Value: "sa:flannel"}, } + + testSigstoreSelectors = []*common.Selector{ + {Type: "k8s", Value: "container-image:docker-pullable://localhost/spiffe/blog@sha256:0cfdaced91cb46dd7af48309799a3c351e4ca2d5e1ee9737ca0cbd932cb79898"}, + {Type: "k8s", Value: "container-image:localhost/spiffe/blog:latest"}, + {Type: "k8s", Value: "container-name:blog"}, + {Type: "k8s", Value: "docker://9bca8d63d5fa610783847915bcff0ecac1273e5b4bed3f6fa1b07350e0135961:image-signature-subject:sigstore-subject"}, + {Type: "k8s", Value: "node-name:k8s-node-1"}, + {Type: "k8s", Value: "ns:default"}, + {Type: "k8s", Value: "pod-image-count:2"}, + {Type: "k8s", Value: "pod-image:docker-pullable://localhost/spiffe/blog@sha256:0cfdaced91cb46dd7af48309799a3c351e4ca2d5e1ee9737ca0cbd932cb79898"}, + {Type: "k8s", Value: "pod-image:docker-pullable://localhost/spiffe/ghostunnel@sha256:b2fc20676c92a433b9a91f3f4535faddec0c2c3613849ac12f02c1d5cfcd4c3a"}, + {Type: "k8s", Value: "pod-image:localhost/spiffe/blog:latest"}, + {Type: "k8s", Value: "pod-image:localhost/spiffe/ghostunnel:latest"}, + {Type: "k8s", Value: "pod-init-image-count:0"}, + {Type: "k8s", Value: "pod-label:k8s-app:blog"}, + {Type: "k8s", Value: "pod-label:version:v0"}, + {Type: "k8s", Value: "pod-name:blog-24ck7"}, + {Type: "k8s", Value: "pod-owner-uid:ReplicationController:2c401175-b29f-11e7-9350-020968147796"}, + {Type: "k8s", Value: "pod-owner:ReplicationController:blog"}, + {Type: "k8s", Value: "pod-uid:2c48913c-b29f-11e7-9350-020968147796"}, + {Type: "k8s", Value: "sa:default"}, + {Type: "k8s", Value: "sigstore-validation:passed"}, + } + + testSigstoreSkippedSelectors = []*common.Selector{ + {Type: "k8s", Value: "container-image:docker-pullable://localhost/spiffe/blog@sha256:0cfdaced91cb46dd7af48309799a3c351e4ca2d5e1ee9737ca0cbd932cb79898"}, + {Type: "k8s", Value: "container-image:localhost/spiffe/blog:latest"}, + {Type: "k8s", Value: "container-name:blog"}, + {Type: "k8s", Value: "node-name:k8s-node-1"}, + {Type: "k8s", Value: "ns:default"}, + {Type: "k8s", Value: "pod-image-count:2"}, + {Type: "k8s", Value: "pod-image:docker-pullable://localhost/spiffe/blog@sha256:0cfdaced91cb46dd7af48309799a3c351e4ca2d5e1ee9737ca0cbd932cb79898"}, + {Type: "k8s", Value: "pod-image:docker-pullable://localhost/spiffe/ghostunnel@sha256:b2fc20676c92a433b9a91f3f4535faddec0c2c3613849ac12f02c1d5cfcd4c3a"}, + {Type: "k8s", Value: "pod-image:localhost/spiffe/blog:latest"}, + {Type: "k8s", Value: "pod-image:localhost/spiffe/ghostunnel:latest"}, + {Type: "k8s", Value: "pod-init-image-count:0"}, + {Type: "k8s", Value: "pod-label:k8s-app:blog"}, + {Type: "k8s", Value: "pod-label:version:v0"}, + {Type: "k8s", Value: "pod-name:blog-24ck7"}, + {Type: "k8s", Value: "pod-owner-uid:ReplicationController:2c401175-b29f-11e7-9350-020968147796"}, + {Type: "k8s", Value: "pod-owner:ReplicationController:blog"}, + {Type: "k8s", Value: "pod-uid:2c48913c-b29f-11e7-9350-020968147796"}, + {Type: "k8s", Value: "sa:default"}, + {Type: "k8s", Value: "sigstore-validation:passed"}, + } ) func (s *Suite) TestAttestWithInitPidInPod() { @@ -152,6 +205,176 @@ func (s *Suite) TestAttestAgainstNodeOverride() { s.Require().Empty(selectors) } +func (s *Suite) TestFailedToCreateHelperFromConfigure() { + t := s.T() + p := s.newPlugin() + + var err error + plugintest.Load(t, builtin(p), nil, + plugintest.Configure(` + experimental = { + sigstore = { + rekor_url = "inva{{{lid}" + } + } + `), + plugintest.CaptureConfigureError(&err)) + spiretest.RequireGRPCStatus(t, err, codes.InvalidArgument, "failed to set Rekor URL: host is required on rekor URL") +} + +func (s *Suite) TestHelperConfigure() { + rekorURL := "https://rekor.example.com/" + invalidURL := "invalid url" + for _, tt := range []struct { + name string + config *HCLConfig + errCode codes.Code + errMsg string + clientErr error + + expectSkippedImages map[string]struct{} + expectRekoURL string + expectSubjects map[string]map[string]struct{} + }{ + { + name: "sigstore is configured", + config: &HCLConfig{ + Experimental: &ExperimentalK8SConfig{ + Sigstore: &SigstoreHCLConfig{ + RekorURL: &rekorURL, + SkippedImages: []string{"sha:image1hash", "sha:image2hash"}, + AllowedSubjects: map[string][]string{"issuer": {"spirex@example.com", "spirex1@example.com"}}, + }, + }, + }, + expectRekoURL: rekorURL, + expectSkippedImages: map[string]struct{}{ + "sha:image1hash": {}, + "sha:image2hash": {}, + }, + expectSubjects: map[string]map[string]struct{}{ + "issuer": { + "spirex@example.com": {}, + "spirex1@example.com": {}, + }, + }, + }, + { + name: "only reko url", + config: &HCLConfig{ + Experimental: &ExperimentalK8SConfig{ + Sigstore: &SigstoreHCLConfig{ + RekorURL: &rekorURL, + }, + }, + }, + expectRekoURL: rekorURL, + }, + { + name: "missing url, use default", + config: &HCLConfig{ + Experimental: &ExperimentalK8SConfig{ + Sigstore: &SigstoreHCLConfig{ + RekorURL: nil, + }, + }, + }, + errCode: codes.InvalidArgument, + errMsg: "missing Rekor URL", + }, + { + name: "failed to set url", + config: &HCLConfig{ + Experimental: &ExperimentalK8SConfig{ + Sigstore: &SigstoreHCLConfig{ + RekorURL: &invalidURL, + }, + }, + }, + clientErr: errors.New("oh no"), + errCode: codes.InvalidArgument, + errMsg: "failed to set Rekor URL: oh no", + }, + } { + s.T().Run(tt.name, func(t *testing.T) { + fakeClient := &sigstoreMock{ + returnError: tt.clientErr, + } + h := &containerHelper{ + sigstoreClient: fakeClient, + } + + err := h.Configure(tt.config, hclog.NewNullLogger()) + + if tt.errMsg != "" { + spiretest.RequireGRPCStatus(t, err, tt.errCode, tt.errMsg) + return + } + + require.NoError(t, err) + require.NotNil(t, h.sigstoreClient) + + require.Equal(t, tt.expectSkippedImages, fakeClient.skippedImages) + require.Equal(t, tt.expectRekoURL, fakeClient.rekorURL) + require.Equal(t, tt.expectSubjects, fakeClient.allowedSubjects) + }) + } +} + +func (s *Suite) TestAttestWithSigstoreSignatures() { + s.startInsecureKubelet() + s.oc.fakeClient.selectors = []sigstore.SelectorsFromSignatures{ + { + Subject: "sigstore-subject", + }, + } + p := s.loadInsecurePlugin() + s.requireAttestSuccessWithPodAndSignature(p) +} + +func (s *Suite) TestAttestWithSigstoreSkippedImage() { + s.startInsecureKubelet() + // Skip the image + s.oc.fakeClient.rekorURL = "reo" + s.oc.fakeClient.skipSigs = true + s.oc.fakeClient.skippedSigSelectors = []string{"sigstore-validation:passed"} + p := s.loadInsecurePlugin() + s.requireAttestSuccessWithPodAndSkippedImage(p) +} + +func (s *Suite) TestAttestWithFailedSigstoreSignatures() { + s.startInsecureKubelet() + + p := s.newPlugin() + + v1 := new(workloadattestor.V1) + plugintest.Load(s.T(), builtin(p), v1, + plugintest.Configure(fmt.Sprintf(` + kubelet_read_only_port = %d + max_poll_attempts = 5 + poll_retry_interval = "1s" + `, s.kubeletPort())), + ) + + if cHelper := s.oc.getContainerHelper(p); cHelper != nil { + p.setContainerHelper(cHelper) + } + + buf := bytes.Buffer{} + newLog := hclog.New(&hclog.LoggerOptions{ + Output: &buf, + }) + + p.SetLogger(newLog) + + s.oc.fakeClient.returnError = errors.New("sigstore error 123") + + s.requireAttestFailureWithPod(v1, codes.Internal, "error retrieving signature payload: sigstore error 123") + logString := buf.String() + s.Require().Contains(logString, "Error retrieving signature payload") + s.Require().Contains(logString, "error=\"sigstore error 123\"") +} + func (s *Suite) TestAttestWhenContainerNotReadyButContainerSelectorsDisabled() { // This test will not pass on windows since obtaining the container ID is // currently required to identify the workload pod in that environment. @@ -415,13 +638,144 @@ func TestGetPodUIDAndContainerIDFromCGroupPath(t *testing.T) { } } +func (s *Suite) requireAttestSuccessWithPodAndSignature(p workloadattestor.WorkloadAttestor) { + s.addPodListResponse(podListFilePath) + s.addCgroupsResponse(cgPidInPodFilePath) + s.requireAttestSuccess(p, testSigstoreSelectors) +} + +func (s *Suite) requireAttestSuccessWithPodAndSkippedImage(p workloadattestor.WorkloadAttestor) { + s.addPodListResponse(podListFilePath) + s.addCgroupsResponse(cgPidInPodFilePath) + s.requireAttestSuccess(p, testSigstoreSkippedSelectors) +} + +func (s *Suite) requireAttestFailureWithPod(p workloadattestor.WorkloadAttestor, code codes.Code, contains string) { + s.addPodListResponse(podListFilePath) + s.addGetContainerResponsePidInPod() + s.requireAttestFailure(p, code, contains) +} + type osConfig struct { + fakeClient *sigstoreMock } -func (o *osConfig) getContainerHelper() ContainerHelper { - return nil +func (o *osConfig) getContainerHelper(p *Plugin) ContainerHelper { + return &containerHelper{ + fs: p.fs, + sigstoreClient: o.fakeClient, + } } func createOSConfig() *osConfig { - return &osConfig{} + return &osConfig{ + fakeClient: &sigstoreMock{}, + } +} + +type sigstoreMock struct { + selectors []sigstore.SelectorsFromSignatures + + sigs []oci.Signature + skipSigs bool + skippedSigSelectors []string + returnError error + skippedImages map[string]struct{} + allowedSubjects map[string]map[string]struct{} + log hclog.Logger + + rekorURL string + enforceSCT bool +} + +// SetLogger implements sigstore.Sigstore +func (s *sigstoreMock) SetLogger(logger hclog.Logger) { + s.log = logger +} + +func (s *sigstoreMock) SetEnforceSCT(enforceSCT bool) { + s.enforceSCT = enforceSCT +} + +func (s *sigstoreMock) FetchImageSignatures(ctx context.Context, imageName string) ([]oci.Signature, error) { + if s.returnError != nil { + return nil, s.returnError + } + return s.sigs, nil +} + +func (s *sigstoreMock) SelectorValuesFromSignature(signatures oci.Signature) (*sigstore.SelectorsFromSignatures, error) { + if len(s.selectors) != 0 { + return &s.selectors[0], nil + } + return nil, s.returnError +} + +func (s *sigstoreMock) ExtractSelectorsFromSignatures(signatures []oci.Signature, containerID string) []sigstore.SelectorsFromSignatures { + return s.selectors +} + +func (s *sigstoreMock) ShouldSkipImage(imageID string) (bool, error) { + return s.skipSigs, s.returnError +} + +func (s *sigstoreMock) ClearSkipList() { + s.skippedImages = nil +} + +func (s *sigstoreMock) ClearAllowedSubjects() { + s.allowedSubjects = nil +} + +func (s *sigstoreMock) AttestContainerSignatures(ctx context.Context, status *corev1.ContainerStatus) ([]string, error) { + if s.skipSigs { + return s.skippedSigSelectors, nil + } + if s.returnError != nil { + return nil, s.returnError + } + var selectorsString []string + for _, selector := range s.selectors { + if selector.Subject != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-subject:%s", status.ContainerID, selector.Subject)) + } + if selector.Content != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-content:%s", status.ContainerID, selector.Content)) + } + if selector.LogID != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-logid:%s", status.ContainerID, selector.LogID)) + } + if selector.IntegratedTime != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-integrated-time:%s", status.ContainerID, selector.IntegratedTime)) + } + selectorsString = append(selectorsString, "sigstore-validation:passed") + } + return selectorsString, nil +} + +func (s *sigstoreMock) SetRekorURL(url string) error { + if s.returnError != nil { + return s.returnError + } + s.rekorURL = url + return s.returnError +} + +func (s *sigstoreMock) AddAllowedSubject(issuer string, subject string) { + if s.allowedSubjects == nil { + s.allowedSubjects = make(map[string]map[string]struct{}) + } + if _, ok := s.allowedSubjects[issuer]; !ok { + s.allowedSubjects[issuer] = make(map[string]struct{}) + } + s.allowedSubjects[issuer][subject] = struct{}{} +} + +func (s *sigstoreMock) AddSkippedImages(images []string) { + if s.skippedImages == nil { + s.skippedImages = make(map[string]struct{}) + } + for _, imageID := range images { + s.skippedImages[imageID] = struct{}{} + } } diff --git a/pkg/agent/plugin/workloadattestor/k8s/k8s_test.go b/pkg/agent/plugin/workloadattestor/k8s/k8s_test.go index be0dfc5bba..f8ed846167 100644 --- a/pkg/agent/plugin/workloadattestor/k8s/k8s_test.go +++ b/pkg/agent/plugin/workloadattestor/k8s/k8s_test.go @@ -109,12 +109,11 @@ type Suite struct { func (s *Suite) SetupTest() { s.dir = s.TempDir() s.writeFile(defaultTokenPath, "default-token") - s.clock = clock.NewMock(s.T()) s.server = nil - s.podList = nil s.env = map[string]string{} + s.oc = createOSConfig() } @@ -563,6 +562,7 @@ func (s *Suite) newPlugin() *Plugin { p.getenv = func(key string) string { return s.env[key] } + return p } @@ -604,7 +604,7 @@ func (s *Suite) loadPlugin(configuration string) workloadattestor.WorkloadAttest plugintest.Configure(configuration), ) - if cHelper := s.oc.getContainerHelper(); cHelper != nil { + if cHelper := s.oc.getContainerHelper(p); cHelper != nil { p.setContainerHelper(cHelper) } return v1 diff --git a/pkg/agent/plugin/workloadattestor/k8s/k8s_windows.go b/pkg/agent/plugin/workloadattestor/k8s/k8s_windows.go index e18be5f5f6..c3d83a5d39 100644 --- a/pkg/agent/plugin/workloadattestor/k8s/k8s_windows.go +++ b/pkg/agent/plugin/workloadattestor/k8s/k8s_windows.go @@ -4,12 +4,14 @@ package k8s import ( + "context" "path/filepath" "github.com/hashicorp/go-hclog" "github.com/spiffe/spire/pkg/common/container/process" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -17,16 +19,28 @@ const ( containerMountPointEnvVar = "CONTAINER_SANDBOX_MOUNT_POINT" ) -func createHelper(c *Plugin) (ContainerHelper, error) { +func createHelper(p *Plugin) ContainerHelper { return &containerHelper{ ph: process.CreateHelper(), - }, nil + } } type containerHelper struct { ph process.Helper } +func (h *containerHelper) Configure(config *HCLConfig, log hclog.Logger) error { + if config.Experimental != nil && config.Experimental.Sigstore != nil { + return status.Error(codes.InvalidArgument, "sigstore configuration is not supported on windows environment") + } + return nil +} + +func (h *containerHelper) GetOSSelectors(ctx context.Context, log hclog.Logger, containerStatus *corev1.ContainerStatus) ([]string, error) { + // No additional selectors on windows + return nil, nil +} + func (h *containerHelper) GetPodUIDAndContainerID(pID int32, log hclog.Logger) (types.UID, string, error) { containerID, err := h.ph.GetContainerIDByProcess(pID, log) if err != nil { diff --git a/pkg/agent/plugin/workloadattestor/k8s/k8s_windows_test.go b/pkg/agent/plugin/workloadattestor/k8s/k8s_windows_test.go index a0532e7583..31b87593a4 100644 --- a/pkg/agent/plugin/workloadattestor/k8s/k8s_windows_test.go +++ b/pkg/agent/plugin/workloadattestor/k8s/k8s_windows_test.go @@ -4,14 +4,17 @@ package k8s import ( + "context" "errors" "testing" "github.com/hashicorp/go-hclog" + "github.com/spiffe/spire/test/plugintest" "github.com/spiffe/spire/test/spiretest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -19,7 +22,7 @@ type osConfig struct { cHelper *fakeContainerHelper } -func (o *osConfig) getContainerHelper() ContainerHelper { +func (o *osConfig) getContainerHelper(_ *Plugin) ContainerHelper { return o.cHelper } @@ -30,8 +33,21 @@ func createOSConfig() *osConfig { } type fakeContainerHelper struct { - cIDs map[int32]string - err error + cIDs map[int32]string + err error + osSelectors []string + osError error +} + +func (h *fakeContainerHelper) Configure(config *HCLConfig, log hclog.Logger) error { + return h.err +} + +func (h *fakeContainerHelper) GetOSSelectors(ctx context.Context, log hclog.Logger, containerStatus *corev1.ContainerStatus) ([]string, error) { + if h.osError != nil { + return nil, h.osError + } + return h.osSelectors, nil } func (h *fakeContainerHelper) GetPodUIDAndContainerID(pID int32, _ hclog.Logger) (types.UID, string, error) { @@ -53,6 +69,23 @@ func (s *Suite) addGetContainerResponsePidInPod() { } } +func (s *Suite) TestFailedToStartWhenUsingSigstore() { + t := s.T() + p := s.newPlugin() + + var err error + plugintest.Load(t, builtin(p), nil, + plugintest.Configure(` + experimental = { + sigstore = { + rekor_url = "https://rekor.org" + } + } + `), + plugintest.CaptureConfigureError(&err)) + spiretest.RequireGRPCStatus(t, err, codes.InvalidArgument, "sigstore configuration is not supported on windows environment") +} + func TestContainerHelper(t *testing.T) { fakeHelper := &fakeProcessHelper{} cHelper := &containerHelper{ diff --git a/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstore.go b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstore.go new file mode 100644 index 0000000000..9ea07adfcd --- /dev/null +++ b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstore.go @@ -0,0 +1,483 @@ +//go:build !windows +// +build !windows + +package sigstore + +import ( + "bytes" + "context" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/hashicorp/go-hclog" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" + "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/bundle" + "github.com/sigstore/cosign/pkg/oci" + sig "github.com/sigstore/cosign/pkg/signature" + rekor "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/sigstore/pkg/signature/payload" + "github.com/spiffe/spire/pkg/common/telemetry" + corev1 "k8s.io/api/core/v1" +) + +const ( + // Signature Verification Selector + signatureVerifiedSelector = "sigstore-validation:passed" +) + +var ( + // OIDC token issuer Object Identifier + oidcIssuerOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1} +) + +type Sigstore interface { + AttestContainerSignatures(ctx context.Context, status *corev1.ContainerStatus) ([]string, error) + FetchImageSignatures(ctx context.Context, imageName string) ([]oci.Signature, error) + SelectorValuesFromSignature(oci.Signature) (*SelectorsFromSignatures, error) + ExtractSelectorsFromSignatures(signatures []oci.Signature, containerID string) []SelectorsFromSignatures + ShouldSkipImage(imageID string) (bool, error) + AddSkippedImages(imageID []string) + ClearSkipList() + AddAllowedSubject(issuer string, subject string) + ClearAllowedSubjects() + SetRekorURL(rekorURL string) error + SetLogger(logger hclog.Logger) + SetEnforceSCT(enforceSCT bool) +} + +// The following structs are used to go through the payload json objects +type BundleSignature struct { + Content string `json:"content"` + Format string `json:"format"` + PublicKey map[string]string `json:"publicKey"` +} + +type BundleSpec struct { + Data map[string]map[string]string `json:"data"` + Signature BundleSignature `json:"signature"` +} + +type BundleBody struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Spec BundleSpec `json:"spec"` +} + +// Data extracted from signature +type SelectorsFromSignatures struct { + Subject string + Content string + LogID string + IntegratedTime string +} + +func New(cache Cache, logger hclog.Logger) Sigstore { + return &sigstoreImpl{ + functionHooks: sigstoreFunctionHooks{ + // verifyFunction does all the images signatures checks, returning the verified signatures. If there were no valid signatures, it returns an error. + verifyFunction: cosign.VerifyImageSignatures, + fetchImageManifestFunction: remote.Get, + checkOptsFunction: defaultCheckOptsFunction, + }, + + logger: logger, + sigstorecache: cache, + } +} + +type sigstoreImpl struct { + functionHooks sigstoreFunctionHooks + skippedImages map[string]struct{} + subjectAllowList map[string]map[string]struct{} + rekorURL url.URL + logger hclog.Logger + sigstorecache Cache + enforceSCT bool +} + +type sigstoreFunctionHooks struct { + verifyFunction verifyFunctionType + fetchImageManifestFunction fetchImageManifestFunctionType + checkOptsFunction checkOptsFunctionType +} + +func (s *sigstoreImpl) SetEnforceSCT(enforceSCT bool) { + s.enforceSCT = enforceSCT +} + +func (s *sigstoreImpl) SetLogger(logger hclog.Logger) { + s.logger = logger +} + +// FetchImageSignatures retrieves signatures for specified image via cosign, using the specified rekor server. +// Returns a list of verified signatures, and an error if any. +func (s *sigstoreImpl) FetchImageSignatures(ctx context.Context, imageName string) ([]oci.Signature, error) { + ref, err := name.ParseReference(imageName) + if err != nil { + return nil, fmt.Errorf("error parsing image reference: %w", err) + } + + if err := s.ValidateImage(ref); err != nil { + return nil, fmt.Errorf("could not validate image reference digest: %w", err) + } + + co, err := s.functionHooks.checkOptsFunction(s.rekorURL, s.enforceSCT) + if err != nil { + return nil, fmt.Errorf("could not create cosign check options: %w", err) + } + sigs, ok, err := s.functionHooks.verifyFunction(ctx, ref, co) + switch { + case err != nil: + return nil, fmt.Errorf("error verifying signature: %w", err) + case !ok: + return nil, fmt.Errorf("bundle not verified for %q", imageName) + default: + return sigs, nil + } +} + +// ExtractSelectorsFromSignatures extracts selectors from a list of image signatures. +// returns a list of selector strings. +func (s *sigstoreImpl) ExtractSelectorsFromSignatures(signatures []oci.Signature, containerID string) []SelectorsFromSignatures { + if len(signatures) == 0 { + s.logger.Error("no signatures found for container", telemetry.ContainerID, containerID) + return nil + } + var selectors []SelectorsFromSignatures + for _, sig := range signatures { + // verify which subject + sigSelectors, err := s.SelectorValuesFromSignature(sig) + if err != nil { + s.logger.Error("error extracting selectors from signature", "error", err, telemetry.ContainerID, containerID) + + continue + } + selectors = append(selectors, *sigSelectors) + } + return selectors +} + +// SelectorValuesFromSignature extracts selectors from a signature. +// returns a list of selectors. +func (s *sigstoreImpl) SelectorValuesFromSignature(signature oci.Signature) (*SelectorsFromSignatures, error) { + subject, err := getSignatureSubject(signature) + if err != nil { + return nil, fmt.Errorf("error getting signature subject: %w", err) + } + if subject == "" { + return nil, errors.New("error getting signature subject: empty subject") + } + + issuer, err := getSignatureProvider(signature) + if err != nil { + return nil, fmt.Errorf("error getting signature provider: %w", err) + } + if issuer == "" { + return nil, fmt.Errorf("error getting signature provider: %w", errors.New("empty issuer")) + } + + if issuerSubjects, ok := s.subjectAllowList[issuer]; !ok { + return nil, fmt.Errorf("signature issuer %q not in allow-list", issuer) + } else if _, ok := issuerSubjects[subject]; !ok { + return nil, fmt.Errorf("subject %q not allowed for issuer %q", subject, issuer) + } + + bundle, err := signature.Bundle() + switch { + case err != nil: + return nil, fmt.Errorf("error getting signature bundle: %w", err) + case bundle.Payload.LogID == "": + return nil, errors.New("error getting signature log ID: empty log ID") + case bundle.Payload.IntegratedTime == 0: + return nil, errors.New("error getting signature integrated time: integrated time is 0") + } + sigContent, err := getBundleSignatureContent(bundle) + if err != nil { + return nil, fmt.Errorf("error getting signature content: %w", err) + } + + return &SelectorsFromSignatures{ + Subject: subject, + Content: sigContent, + LogID: bundle.Payload.LogID, + IntegratedTime: strconv.FormatInt(bundle.Payload.IntegratedTime, 10), + }, nil +} + +// ShouldSkipImage checks the skip list for the image ID in the container status. +// If the image ID is found in the skip list, it returns true. +// If the image ID is not found in the skip list, it returns false. +func (s *sigstoreImpl) ShouldSkipImage(imageID string) (bool, error) { + if imageID == "" { + return false, errors.New("image ID is empty") + } + if len(s.skippedImages) == 0 { + return false, nil + } + _, ok := s.skippedImages[imageID] + return ok, nil +} + +// AddSkippedImage adds the image ID and selectors to the skip list. +func (s *sigstoreImpl) AddSkippedImages(imageIDList []string) { + if s.skippedImages == nil { + s.skippedImages = make(map[string]struct{}) + } + for _, imageID := range imageIDList { + s.skippedImages[imageID] = struct{}{} + } +} + +// ClearSkipList clears the skip list. +func (s *sigstoreImpl) ClearSkipList() { + s.skippedImages = nil +} + +// ValidateImage validates if the image manifest hash matches the digest in the image reference +func (s *sigstoreImpl) ValidateImage(ref name.Reference) error { + dgst, ok := ref.(name.Digest) + if !ok { + return fmt.Errorf("reference %T is not a digest", ref) + } + desc, err := s.functionHooks.fetchImageManifestFunction(dgst) + if err != nil { + return err + } + if len(desc.Manifest) == 0 { + return errors.New("manifest is empty") + } + hash, _, err := v1.SHA256(bytes.NewReader(desc.Manifest)) + if err != nil { + return err + } + + return validateRefDigest(dgst, hash.String()) +} + +func (s *sigstoreImpl) AddAllowedSubject(issuer string, subject string) { + if s.subjectAllowList == nil { + s.subjectAllowList = make(map[string]map[string]struct{}) + } + if _, ok := s.subjectAllowList[issuer]; !ok { + s.subjectAllowList[issuer] = make(map[string]struct{}) + } + s.subjectAllowList[issuer][subject] = struct{}{} +} + +func (s *sigstoreImpl) ClearAllowedSubjects() { + s.subjectAllowList = nil +} + +func (s *sigstoreImpl) AttestContainerSignatures(ctx context.Context, status *corev1.ContainerStatus) ([]string, error) { + skip, err := s.ShouldSkipImage(status.ImageID) + if err != nil { + return nil, fmt.Errorf("failed attesting container signature: %w", err) + } + if skip { + return []string{signatureVerifiedSelector}, nil + } + + imageID := status.ImageID + + cachedSignature := s.sigstorecache.GetSignature(imageID) + if cachedSignature != nil { + s.logger.Debug("Found cached signature", "image_id", imageID) + } else { + signatures, err := s.FetchImageSignatures(ctx, imageID) + if err != nil { + return nil, err + } + + selectors := s.ExtractSelectorsFromSignatures(signatures, status.ContainerID) + + cachedSignature = &Item{ + Key: imageID, + Value: selectors, + } + + s.logger.Debug("Caching signature", "image_id", imageID) + s.sigstorecache.PutSignature(*cachedSignature) + } + + var selectorsString []string + if len(cachedSignature.Value) > 0 { + for _, selector := range cachedSignature.Value { + toString := selectorsToString(selector, status.ContainerID) + selectorsString = append(selectorsString, toString...) + } + selectorsString = append(selectorsString, signatureVerifiedSelector) + } + + return selectorsString, nil +} + +func (s *sigstoreImpl) SetRekorURL(rekorURL string) error { + if rekorURL == "" { + return errors.New("rekor URL is empty") + } + rekorURI, err := url.Parse(rekorURL) + if err != nil { + return fmt.Errorf("failed parsing rekor URI: %w", err) + } + if rekorURI.Host == "" { + return fmt.Errorf("host is required on rekor URL") + } + if rekorURI.Scheme != "https" { + return fmt.Errorf("invalid rekor URL Scheme %q", rekorURI.Scheme) + } + s.rekorURL = *rekorURI + return nil +} + +func defaultCheckOptsFunction(rekorURL url.URL, enforceSCT bool) (*cosign.CheckOpts, error) { + switch { + case rekorURL.Host == "": + return nil, errors.New("rekor URL host is empty") + case rekorURL.Scheme == "": + return nil, errors.New("rekor URL scheme is empty") + case rekorURL.Path == "": + return nil, errors.New("rekor URL path is empty") + } + + rootCerts, err := fulcio.GetRoots() + if err != nil { + return nil, fmt.Errorf("failed to get fulcio root certificates: %w", err) + } + + cfg := rekor.DefaultTransportConfig().WithBasePath(rekorURL.Path).WithHost(rekorURL.Host) + co := &cosign.CheckOpts{ + // Set the rekor client + RekorClient: rekor.NewHTTPClientWithConfig(nil, cfg), + RootCerts: rootCerts, + EnforceSCT: enforceSCT, + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + + return co, err +} + +func getSignatureSubject(signature oci.Signature) (string, error) { + if signature == nil { + return "", errors.New("signature is nil") + } + ss := payload.SimpleContainerImage{} + pl, err := signature.Payload() + if err != nil { + return "", err + } + if pl == nil { + return "", errors.New("signature payload is nil") + } + if err := json.Unmarshal(pl, &ss); err != nil { + return "", err + } + cert, err := signature.Cert() + if err != nil { + return "", fmt.Errorf("failed to access signature certificate: %w", err) + } + + if cert != nil { + return sig.CertSubject(cert), nil + } + if len(ss.Optional) > 0 { + if subjString, ok := ss.Optional["subject"]; ok { + if subj, ok := subjString.(string); ok { + return subj, nil + } + } + } + + return "", errors.New("no subject found in signature") +} + +func getSignatureProvider(signature oci.Signature) (string, error) { + if signature == nil { + return "", errors.New("signature is nil") + } + cert, err := signature.Cert() + if err != nil { + return "", fmt.Errorf("failed to access signature certificate: %w", err) + } + if cert == nil { + return "", errors.New("no certificate found in signature") + } + return certOIDCProvider(cert) +} + +func certOIDCProvider(cert *x509.Certificate) (string, error) { + if cert == nil { + return "", errors.New("certificate is nil") + } + + for _, ext := range cert.Extensions { + if ext.Id.Equal(oidcIssuerOID) { + return string(ext.Value), nil + } + } + + return "", errors.New("no OIDC issuer found in certificate extensions") +} + +func getBundleSignatureContent(bundle *bundle.RekorBundle) (string, error) { + if bundle == nil { + return "", errors.New("bundle is nil") + } + body64, ok := bundle.Payload.Body.(string) + if !ok { + return "", fmt.Errorf("expected payload body to be a string but got %T instead", bundle.Payload.Body) + } + body, err := base64.StdEncoding.DecodeString(body64) + if err != nil { + return "", err + } + var bundleBody BundleBody + if err := json.Unmarshal(body, &bundleBody); err != nil { + return "", fmt.Errorf("failed to parse bundle body: %w", err) + } + + if bundleBody.Spec.Signature.Content == "" { + return "", errors.New("bundle payload body has no signature content") + } + + return bundleBody.Spec.Signature.Content, nil +} + +func selectorsToString(selectors SelectorsFromSignatures, containerID string) []string { + var selectorsString []string + if selectors.Subject != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-subject:%s", containerID, selectors.Subject)) + } + if selectors.Content != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-content:%s", containerID, selectors.Content)) + } + if selectors.LogID != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-logid:%s", containerID, selectors.LogID)) + } + if selectors.IntegratedTime != "" { + selectorsString = append(selectorsString, fmt.Sprintf("%s:image-signature-integrated-time:%s", containerID, selectors.IntegratedTime)) + } + return selectorsString +} + +func validateRefDigest(dgst name.Digest, digest string) error { + if dgst.DigestStr() == digest { + return nil + } + return fmt.Errorf("digest %s does not match %s", digest, dgst.DigestStr()) +} + +type verifyFunctionType func(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error) + +type fetchImageManifestFunctionType func(name.Reference, ...remote.Option) (*remote.Descriptor, error) + +type checkOptsFunctionType func(url.URL, bool) (*cosign.CheckOpts, error) diff --git a/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstore_test.go b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstore_test.go new file mode 100644 index 0000000000..40529455e4 --- /dev/null +++ b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstore_test.go @@ -0,0 +1,1998 @@ +//go:build !windows +// +build !windows + +package sigstore + +import ( + "bytes" + "context" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "fmt" + "net/url" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/hashicorp/go-hclog" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" + "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/bundle" + "github.com/sigstore/cosign/pkg/oci" + rekor "github.com/sigstore/rekor/pkg/generated/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +const ( + maximumAmountCache = 10 +) + +func TestNew(t *testing.T) { + newcache := NewCache(maximumAmountCache) + want := &sigstoreImpl{ + functionHooks: sigstoreFunctionHooks{ + verifyFunction: cosign.VerifyImageSignatures, + fetchImageManifestFunction: remote.Get, + checkOptsFunction: defaultCheckOptsFunction, + }, + skippedImages: nil, + subjectAllowList: nil, + rekorURL: url.URL{}, + sigstorecache: newcache, + logger: nil, + } + sigstore := New(newcache, nil) + + require.IsType(t, &sigstoreImpl{}, sigstore) + sigImpObj, _ := sigstore.(*sigstoreImpl) + + // test each field manually since require.Equal does not work on function pointers + if &(sigImpObj.functionHooks.verifyFunction) == &(want.functionHooks.verifyFunction) { + t.Errorf("verify functions do not match") + } + if &(sigImpObj.functionHooks.fetchImageManifestFunction) == &(want.functionHooks.fetchImageManifestFunction) { + t.Errorf("fetchImageManifest functions do not match") + } + if &(sigImpObj.functionHooks.checkOptsFunction) == &(want.functionHooks.checkOptsFunction) { + t.Errorf("checkOptsFunction functions do not match") + } + require.Empty(t, sigImpObj.skippedImages, "skippedImages array is not empty") + require.Empty(t, sigImpObj.subjectAllowList, "subjectAllowList array is not empty") + require.Equal(t, want.rekorURL, sigImpObj.rekorURL, "rekorURL is different from rekor default") + require.Equal(t, want.sigstorecache, sigImpObj.sigstorecache, "sigstorecache is different from fresh object") + require.Nil(t, sigImpObj.logger, "new logger is not nil") +} + +func TestSigstoreimpl_FetchImageSignatures(t *testing.T) { + type fields struct { + functionBindings sigstoreFunctionBindings + rekorURL url.URL + } + enforceSCT := true + + defaultCheckOpts, err := defaultCheckOptsFunction(rekorDefaultURL(), enforceSCT) + require.NoError(t, err) + emptyURLCheckOpts, emptyError := defaultCheckOptsFunction(url.URL{}, enforceSCT) + require.Nil(t, emptyURLCheckOpts) + require.EqualError(t, emptyError, "rekor URL host is empty") + + tests := []struct { + name string + fields fields + imageName string + wantedFetchArguments fetchFunctionArguments + wantedVerifyArguments verifyFunctionArguments + wantedCheckOptsArguments checkOptsFunctionArguments + want []oci.Signature + wantedErr error + }{ + { + name: "fetch image with signature", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction([]oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + }, + }, true, nil), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + }, + }, + }, + { + name: "fetch image with 2 signatures", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction([]oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + }, + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 4","key3": "value 5"}}`), + }, + }, true, nil), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + }, + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 4","key3": "value 5"}}`), + }, + }, + }, + { + name: "fetch image with no signature", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction(nil, true, errors.New("no matching signatures 2")), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: nil, + wantedErr: fmt.Errorf("error verifying signature: %w", errors.New("no matching signatures 2")), + }, + { // TODO: check again, same as above test. should never happen, since the verify function returns an error on empty verified signature list + name: "fetch image with no signature and no error", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction(nil, true, nil), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: nil, + wantedErr: nil, + }, + { + name: "fetch image with signature and error", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction([]oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + }, + }, true, errors.New("unexpected error")), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: nil, + wantedErr: fmt.Errorf("error verifying signature: %w", errors.New("unexpected error")), + }, + { + name: "fetch image with signature no error, bundle not verified", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction([]oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + }, + }, false, nil), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: nil, + wantedErr: fmt.Errorf("bundle not verified for %q", "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + }, + { + name: "fetch image with invalid image reference", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createNilVerifyFunction(), + fetchBinding: createNilFetchFunction(), + checkOptsBinding: createNilCheckOptsFunction(), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "invali|].url.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + want: nil, + wantedErr: fmt.Errorf("error parsing image reference: %w", errors.New("could not parse reference: invali|].url.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505")), + }, + { + name: "fetch image with signature, empty rekor url", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createNilVerifyFunction(), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createCheckOptsFunction(emptyURLCheckOpts, emptyError), + }, + rekorURL: url.URL{}, + }, + imageName: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{}, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: url.URL{}, + }, + want: nil, + wantedErr: fmt.Errorf("could not create cosign check options: %w", emptyError), + }, + { + name: "fetch image with wrong image hash", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createNilVerifyFunction(), + fetchBinding: createFetchFunction(&remote.Descriptor{Manifest: []byte("sometext")}, nil), + checkOptsBinding: createNilCheckOptsFunction(), + }, + rekorURL: rekorDefaultURL(), + }, + imageName: "docker-registry.com/some/image@sha256:4fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:4fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{}, + wantedCheckOptsArguments: checkOptsFunctionArguments{}, + want: nil, + wantedErr: fmt.Errorf("could not validate image reference digest: %w", errors.New("digest sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505 does not match sha256:4fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fetchArguments := &fetchFunctionArguments{} + verifyArguments := &verifyFunctionArguments{} + checkOptsArguments := &checkOptsFunctionArguments{} + sigstore := sigstoreImpl{ + functionHooks: sigstoreFunctionHooks{ + verifyFunction: tt.fields.functionBindings.verifyBinding(t, verifyArguments), + fetchImageManifestFunction: tt.fields.functionBindings.fetchBinding(t, fetchArguments), + checkOptsFunction: tt.fields.functionBindings.checkOptsBinding(t, checkOptsArguments), + }, + sigstorecache: NewCache(maximumAmountCache), + rekorURL: tt.fields.rekorURL, + } + got, err := sigstore.FetchImageSignatures(context.Background(), tt.imageName) + if tt.wantedErr != nil { + require.EqualError(t, err, tt.wantedErr.Error()) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.want, got) + + require.Equal(t, tt.wantedFetchArguments, *fetchArguments) + + require.Equal(t, tt.wantedCheckOptsArguments, *checkOptsArguments) + + require.Equal(t, tt.wantedVerifyArguments, *verifyArguments) + }) + } +} + +func TestSigstoreimpl_ExtractSelectorsFromSignatures(t *testing.T) { + tests := []struct { + name string + signatures []oci.Signature + containerID string + subjectAllowList map[string]map[string]struct{} + want []SelectorsFromSignatures + wantLog string + }{ + { + name: "extract selector from single image signature array", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + }, + containerID: "000000", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + want: []SelectorsFromSignatures{ + { + Subject: "spirex@example.com", + Content: "MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=", + LogID: "samplelogID", + IntegratedTime: "12345", + }, + }, + }, + { + name: "extract selector from image signature array with multiple entries", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex1@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID1", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex1@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex2@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUI9IgogICAgfQogIH0KfQo=", + LogID: "samplelogID2", + IntegratedTime: 12346, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex2@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + }, + containerID: "111111", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex1@example.com": struct{}{}, "spirex2@example.com": struct{}{}}, + }, + want: []SelectorsFromSignatures{ + { + Subject: "spirex1@example.com", + Content: "MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=", + LogID: "samplelogID1", + IntegratedTime: "12345", + }, + { + Subject: "spirex2@example.com", + Content: "MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smB=", + LogID: "samplelogID2", + IntegratedTime: "12346", + }, + }, + }, + { + name: "with nil payload", + signatures: []oci.Signature{ + signature{ + payload: nil, + }, + }, + containerID: "222222", + want: nil, + wantLog: "signature payload is nil", + }, + { + name: "extract selector from image signature with subject certificate", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "some reference"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"}}`), + cert: &x509.Certificate{ + EmailAddresses: []string{ + "spirex@example.com", + "spirex2@example.com", + }, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + }, + }, + containerID: "333333", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}, "spirex2@example.com": struct{}{}}, + }, + want: []SelectorsFromSignatures{ + { + Subject: "spirex@example.com", + Content: "MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=", + LogID: "samplelogID", + IntegratedTime: "12345", + }, + }, + }, + { + name: "extract selector from image signature with URI certificate", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "some reference"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"}}`), + cert: &x509.Certificate{ + URIs: []*url.URL{ + { + Scheme: "https", + Host: "www.example.com", + Path: "somepath1", + }, + { + Scheme: "https", + Host: "www.spirex.com", + Path: "somepath2", + }, + }, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + }, + }, + containerID: "444444", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"https://www.example.com/somepath1": struct{}{}}, + }, + want: []SelectorsFromSignatures{ + { + Subject: "https://www.example.com/somepath1", + Content: "MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=", + LogID: "samplelogID", + IntegratedTime: "12345", + }, + }, + }, + { + name: "extract selector from empty array", + signatures: []oci.Signature{}, + containerID: "555555", + want: nil, + wantLog: "no signatures found for container: container_id=555555", + }, + { + name: "extract selector from nil array", + signatures: nil, + containerID: "666666", + want: nil, + wantLog: "no signatures found for container: container_id=666666", + }, + { + name: "invalid payload", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{a"critical": {}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + }, + }, + containerID: "777777", + want: nil, + wantLog: "error getting signature subject: invalid character 'a' looking for beginning of object key string", + }, + { + name: "extract selector from single image signature array with error getting provider", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: nil, + }, + }, + containerID: "888888", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + want: nil, + wantLog: "error extracting selectors from signature: error=\"error getting signature provider: no certificate found in signature\" container_id=888888", + }, + { + name: "extract selector from single image signature array with empty provider", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(``), + }}, + }, + }, + }, + containerID: "999999", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + want: nil, + wantLog: "error extracting selectors from signature: error=\"error getting signature provider: empty issuer\" container_id=999999", + }, + { + name: "extract selector from single image signature array with no provider extension", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, Extensions: []pkix.Extension{}, + }, + }, + }, + containerID: "101010", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + want: nil, + wantLog: "error extracting selectors from signature: error=\"error getting signature provider: no OIDC issuer found in certificate extensions\" container_id=101010", + }, + { + name: "extract selector from single image signature array, error no log id", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + }, + containerID: "101101", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + want: nil, + wantLog: "error extracting selectors from signature: error=\"error getting signature log ID: empty log ID\" container_id=101101", + }, + { + name: "extract selector from single image signature array, error no integrated time", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 0, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + }, + containerID: "121212", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + want: nil, + wantLog: "error extracting selectors from signature: error=\"error getting signature integrated time: integrated time is 0\" container_id=121212", + }, + { + name: "extract selector from single image signature array, issuer not in allowlist", + signatures: []oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer2`), + }}, + }, + }, + }, + containerID: "131313", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + want: nil, + wantLog: "error extracting selectors from signature: error=\"signature issuer \\\"issuer2\\\" not in allow-list\" container_id=131313", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.Buffer{} + newLog := hclog.New(&hclog.LoggerOptions{ + Output: &buf, + }) + s := sigstoreImpl{ + logger: newLog, + subjectAllowList: tt.subjectAllowList, + } + got := s.ExtractSelectorsFromSignatures(tt.signatures, tt.containerID) + require.Equal(t, tt.want, got) + if len(tt.wantLog) > 0 { + require.Contains(t, buf.String(), tt.wantLog) + } + }) + } +} + +func TestSigstoreimpl_ShouldSkipImage(t *testing.T) { + tests := []struct { + name string + skippedImages map[string]struct{} + imageID string + want bool + wantedErr error + }{ + { + name: "skipping only image in list", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash": {}, + }, + imageID: "sha256:sampleimagehash", + want: true, + }, + { + name: "skipping image in list", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash": {}, + "sha256:sampleimagehash2": {}, + "sha256:sampleimagehash3": {}, + }, + imageID: "sha256:sampleimagehash2", + want: true, + }, + { + name: "image not in list", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash": {}, + "sha256:sampleimagehash3": {}, + }, + imageID: "sha256:sampleimagehash2", + want: false, + }, + { + name: "empty skip list", + skippedImages: nil, + imageID: "sha256:sampleimagehash", + want: false, + }, + { + name: "empty imageID", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash": {}, + "sha256:sampleimagehash2": {}, + "sha256:sampleimagehash3": {}, + }, + imageID: "", + want: false, + wantedErr: errors.New("image ID is empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sigstore := sigstoreImpl{ + skippedImages: tt.skippedImages, + } + got, err := sigstore.ShouldSkipImage(tt.imageID) + if tt.wantedErr != nil { + require.EqualError(t, err, tt.wantedErr.Error()) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.want, got) + }) + } +} + +func TestSigstoreimpl_AddSkippedImage(t *testing.T) { + tests := []struct { + name string + skippedImages map[string]struct{} + imageID []string + want map[string]struct{} + }{ + { + name: "add skipped image to empty map", + imageID: []string{"sha256:sampleimagehash"}, + want: map[string]struct{}{ + "sha256:sampleimagehash": {}, + }, + }, + { + name: "add skipped image", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash1": {}, + }, + imageID: []string{"sha256:sampleimagehash"}, + want: map[string]struct{}{ + "sha256:sampleimagehash": {}, + "sha256:sampleimagehash1": {}, + }, + }, + { + name: "add a list of skipped images to empty map", + imageID: []string{"sha256:sampleimagehash", "sha256:sampleimagehash1"}, + want: map[string]struct{}{ + "sha256:sampleimagehash": {}, + "sha256:sampleimagehash1": {}, + }, + }, + { + name: "add a list of skipped images to a existing map", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash": {}, + }, + imageID: []string{"sha256:sampleimagehash1", "sha256:sampleimagehash2"}, + want: map[string]struct{}{ + "sha256:sampleimagehash": {}, + "sha256:sampleimagehash1": {}, + "sha256:sampleimagehash2": {}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sigstore := sigstoreImpl{ + skippedImages: tt.skippedImages, + } + sigstore.AddSkippedImages(tt.imageID) + require.Equal(t, tt.want, sigstore.skippedImages) + }) + } +} + +func TestSigstoreimpl_ClearSkipList(t *testing.T) { + tests := []struct { + skippedImages map[string]struct{} + name string + }{ + { + name: "clear single image in map", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash": {}, + }, + }, + { + name: "clear multiple images map", + skippedImages: map[string]struct{}{ + "sha256:sampleimagehash": {}, + "sha256:sampleimagehash1": {}, + }, + }, + { + name: "clear on empty map", + skippedImages: map[string]struct{}{}, + }, + { + name: "clear on nil map", + skippedImages: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sigstore := &sigstoreImpl{ + skippedImages: tt.skippedImages, + } + sigstore.ClearSkipList() + require.Empty(t, sigstore.skippedImages) + }) + } +} + +func TestSigstoreimpl_ValidateImage(t *testing.T) { + type fields struct { + verifyFunction verifyFunctionBinding + fetchImageManifestFunction fetchFunctionBinding + } + tests := []struct { + name string + fields fields + ref name.Reference + wantedFetchArguments fetchFunctionArguments + wantedVerifyArguments verifyFunctionArguments + wantedErr error + }{ + { + name: "validate image", + fields: fields{ + verifyFunction: createNilVerifyFunction(), + fetchImageManifestFunction: createFetchFunction(&remote.Descriptor{ + Manifest: []byte(`sometext`), + }, nil), + }, + ref: name.MustParseReference("example.com/sampleimage@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("example.com/sampleimage@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{}, + }, + { + name: "error on image manifest fetch", + fields: fields{ + verifyFunction: createNilVerifyFunction(), + fetchImageManifestFunction: createFetchFunction(nil, errors.New("fetch error 123")), + }, + ref: name.MustParseReference("example.com/sampleimage@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("example.com/sampleimage@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedErr: errors.New("fetch error 123"), + }, + { + name: "nil image manifest fetch", + fields: fields{ + verifyFunction: createNilVerifyFunction(), + fetchImageManifestFunction: createFetchFunction(&remote.Descriptor{ + Manifest: nil, + }, nil), + }, + ref: name.MustParseReference("example.com/sampleimage@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("example.com/sampleimage@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedErr: errors.New("manifest is empty"), + }, + { + name: "validate hash manifest", + fields: fields{ + verifyFunction: createNilVerifyFunction(), + fetchImageManifestFunction: createFetchFunction(&remote.Descriptor{ + Manifest: []byte("f0c62edf734ff52ee830c9eeef2ceefad94f7f089706d170f8d9dc64befb57cc"), + }, nil), + }, + ref: name.MustParseReference("example.com/sampleimage@sha256:f037cc8ec4cd38f95478773741fdecd48d721a527d19013031692edbf95fae69"), + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("example.com/sampleimage@sha256:f037cc8ec4cd38f95478773741fdecd48d721a527d19013031692edbf95fae69"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fetchArguments := fetchFunctionArguments{} + verifyArguments := verifyFunctionArguments{} + sigstore := &sigstoreImpl{ + functionHooks: sigstoreFunctionHooks{ + verifyFunction: tt.fields.verifyFunction(t, &verifyArguments), + fetchImageManifestFunction: tt.fields.fetchImageManifestFunction(t, &fetchArguments), + }, + } + + err := sigstore.ValidateImage(tt.ref) + if tt.wantedErr != nil { + require.EqualError(t, err, tt.wantedErr.Error()) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.wantedFetchArguments, fetchArguments) + require.Equal(t, tt.wantedVerifyArguments, verifyArguments) + }) + } +} + +func TestSigstoreimpl_AddAllowedSubject(t *testing.T) { + tests := []struct { + name string + subjectAllowList map[string]map[string]struct{} + issuer string + subject string + want map[string]map[string]struct{} + }{ + { + name: "add allowed subject to nil map", + subjectAllowList: nil, + issuer: "issuer1", + subject: "spirex@example.com", + want: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + }, + { + name: "add allowed subject to empty map", + subjectAllowList: map[string]map[string]struct{}{}, + issuer: "issuer1", + subject: "spirex@example.com", + want: map[string]map[string]struct{}{ + "issuer1": {"spirex@example.com": struct{}{}}, + }, + }, + { + name: "add allowed subject to existing map", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": {"spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + }, + issuer: "issuer1", + subject: "spirex4@example.com", + want: map[string]map[string]struct{}{ + "issuer1": {"spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex4@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + }, + }, + { + name: "add allowed subject to existing map with new issuer", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + }, + + issuer: "issuer2", + subject: "spirex4@example.com", + + want: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + "issuer2": { + "spirex4@example.com": struct{}{}, + }, + }, + }, + { + name: "add existing allowed subject to existing map with new issuer", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex4@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + }, + issuer: "issuer2", + subject: "spirex4@example.com", + want: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex4@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + "issuer2": { + "spirex4@example.com": struct{}{}, + }, + }, + }, + { + name: "add existing allowed subject to existing map with same issuer", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex4@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + }, + issuer: "issuer1", + subject: "spirex4@example.com", + want: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex4@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sigstore := &sigstoreImpl{ + subjectAllowList: tt.subjectAllowList, + } + sigstore.AddAllowedSubject(tt.issuer, tt.subject) + require.Equal(t, tt.want, sigstore.subjectAllowList) + }) + } +} + +func TestSigstoreimpl_ClearAllowedSubjects(t *testing.T) { + tests := []struct { + name string + subjectAllowList map[string]map[string]struct{} + }{ + + { + name: "clear existing map", + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex4@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + }, + }, + { + name: "clear map with multiple issuers", + + subjectAllowList: map[string]map[string]struct{}{ + "issuer1": { + "spirex1@example.com": struct{}{}, + "spirex2@example.com": struct{}{}, + "spirex3@example.com": struct{}{}, + "spirex4@example.com": struct{}{}, + "spirex5@example.com": struct{}{}, + }, + "issuer2": { + "spirex1@example.com": struct{}{}, + "spirex6@example.com": struct{}{}, + }, + }, + }, + { + name: "clear empty map", + subjectAllowList: map[string]map[string]struct{}{}, + }, + { + name: "clear nil map", + subjectAllowList: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sigstore := &sigstoreImpl{ + subjectAllowList: tt.subjectAllowList, + } + sigstore.ClearAllowedSubjects() + require.Empty(t, sigstore.subjectAllowList) + }) + } +} + +func TestSigstoreimpl_SelectorValuesFromSignature(t *testing.T) { + tests := []struct { + name string + signature oci.Signature + containerID string + subjectAllowList map[string][]string + want *SelectorsFromSignatures + wantedErr error + }{ + { + name: "selector from signature", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "000000", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: &SelectorsFromSignatures{ + Subject: "spirex@example.com", + Content: "MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=", + LogID: "samplelogID", + IntegratedTime: "12345", + }, + }, + { + name: "selector from signature, empty subject", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: nil, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "111111", + subjectAllowList: nil, + want: nil, + wantedErr: fmt.Errorf("error getting signature subject: empty subject"), + }, + { + name: "selector from signature, not in allowlist", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex1@example.com","key2": "value 2","key3": "value 3"}}`), + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex1@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "222222", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex2@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("subject %q not allowed for issuer %q", "spirex1@example.com", "issuer1"), + }, + { + name: "selector from signature, allowedlist enabled, in allowlist", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "333333", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: &SelectorsFromSignatures{ + Subject: "spirex@example.com", + Content: "MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=", + LogID: "samplelogID", + IntegratedTime: "12345", + }, + }, + { + name: "selector from signature, allowedlist enabled, in allowlist, empty content", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiIgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "444444", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("error getting signature content: bundle payload body has no signature content"), + }, + { + name: "selector from signature, nil bundle", + signature: nilBundleSignature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "555555", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("error getting signature bundle: no bundle test"), + }, + { + name: "selector from signature, bundle payload body is not a string", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: 42, + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "000000", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("error getting signature content: expected payload body to be a string but got int instead"), + }, + { + name: "selector from signature, bundle payload body is not valid base64", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "abc..........def", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "000000", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("error getting signature content: illegal base64 data at input byte 3"), + }, + { + name: "selector from signature, bundle payload body has no signature content", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICAgInNwZWMiOiB7CiAgICAgICJzaWduYXR1cmUiOiB7CiAgICAgIH0KICAgIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "000000", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("error getting signature content: bundle payload body has no signature content"), + }, + { + name: "selector from signature, bundle payload body signature content is empty", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICAgInNwZWMiOiB7CiAgICAgICAgInNpZ25hdHVyZSI6IHsKICAgICAgICAiY29udGVudCI6ICIiCiAgICAgICAgfQogICAgfQp9", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "000000", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("error getting signature content: bundle payload body has no signature content"), + }, + { + name: "selector from signature, bundle payload body is not a valid JSON", + signature: signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjosLCB7CiAgICAic2lnbmF0dXJlIjogewogICAgICAiY29udGVudCI6ICJNRVVDSVFDeWVtOEdjcjBzUEZNUDdmVFhhekNONTdOY041K01qeEp3OU9vMHgyZU0rQUlnZGdCUDk2Qk8xVGUvTmRiakhiVWViMEJVeWU2ZGVSZ1Z0UUV2NU5vNXNtQT0iCiAgICB9CiAgfQp9", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "000000", + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + want: nil, + wantedErr: fmt.Errorf("error getting signature content: failed to parse bundle body: invalid character ',' looking for beginning of value"), + }, + { + name: "selector from signature, empty signature array", + signature: nil, + containerID: "000000", + want: nil, + wantedErr: errors.New("error getting signature subject: signature is nil"), + }, + { + name: "selector from signature, single image signature, no payload", + signature: noPayloadSignature{}, + containerID: "000000", + want: nil, + wantedErr: errors.New("error getting signature subject: no payload test"), + }, + { + name: "selector from signature, single image signature, no certs", + signature: &noCertSignature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + }, + containerID: "000000", + want: nil, + wantedErr: errors.New("error getting signature subject: failed to access signature certificate: no cert test"), + }, + { + name: "selector from signature, single image signature,garbled subject in signature", + signature: &signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "some digest"},"type": "some type"},"optional": {"subject": "s\\\\||as\0\0aasdasd/....???/.>wd12<><,,,><{}{pirex@example.com","key2": "value 2","key3": "value 3"}}`), + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + containerID: "000000", + want: nil, + wantedErr: errors.New("error getting signature subject: invalid character '0' in string escape code"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sigstore := &sigstoreImpl{ + subjectAllowList: nil, + logger: hclog.Default(), + } + for issuer, subjects := range tt.subjectAllowList { + for _, subject := range subjects { + sigstore.AddAllowedSubject(issuer, subject) + } + } + got, err := sigstore.SelectorValuesFromSignature(tt.signature) + assert.Equal(t, tt.want, got) + if tt.wantedErr != nil { + require.EqualError(t, err, tt.wantedErr.Error()) + return + } + require.NoError(t, err) + }) + } +} + +func TestSigstoreimpl_AttestContainerSignatures(t *testing.T) { + type fields struct { + functionBindings sigstoreFunctionBindings + skippedImages map[string]struct{} + rekorURL url.URL + subjectAllowList map[string][]string + } + + rootCerts, err := fulcio.GetRoots() + require.NoError(t, err) + intermediateCerts, err := fulcio.GetIntermediates() + require.NoError(t, err) + + defaultCheckOpts := &cosign.CheckOpts{ + RekorClient: rekor.NewHTTPClientWithConfig(nil, rekor.DefaultTransportConfig().WithBasePath(rekorDefaultURL().Path).WithHost(rekorDefaultURL().Host)), + RootCerts: rootCerts, + IntermediateCerts: intermediateCerts, + } + var emptyURLCheckOpts *cosign.CheckOpts + emptyError := errors.New("rekor URL host is empty") + + tests := []struct { + name string + fields fields + status corev1.ContainerStatus + wantedFetchArguments fetchFunctionArguments + wantedVerifyArguments verifyFunctionArguments + wantedCheckOptsArguments checkOptsFunctionArguments + want []string + wantedErr error + }{ + { + name: "Attest image with signature", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction([]oci.Signature{ + signature{ + payload: []byte(`{"critical": {"identity": {"docker-reference": "docker-registry.com/some/image"},"image": {"docker-manifest-digest": "02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2"},"type": "some type"},"optional": {"subject": "spirex@example.com","key2": "value 2","key3": "value 3"}}`), + bundle: &bundle.RekorBundle{ + Payload: bundle.RekorPayload{ + Body: "ewogICJzcGVjIjogewogICAgInNpZ25hdHVyZSI6IHsKICAgICAgImNvbnRlbnQiOiAiTUVVQ0lRQ3llbThHY3Iwc1BGTVA3ZlRYYXpDTjU3TmNONStNanhKdzlPbzB4MmVNK0FJZ2RnQlA5NkJPMVRlL05kYmpIYlVlYjBCVXllNmRlUmdWdFFFdjVObzVzbUE9IgogICAgfQogIH0KfQ==", + LogID: "samplelogID", + IntegratedTime: 12345, + }, + }, + cert: &x509.Certificate{ + EmailAddresses: []string{"spirex@example.com"}, + Extensions: []pkix.Extension{{ + Id: oidcIssuerOID, + Value: []byte(`issuer1`), + }}, + }, + }, + }, true, nil), + fetchBinding: createFetchFunction(&remote.Descriptor{ + Manifest: []byte("sometext"), + }, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + subjectAllowList: map[string][]string{ + "issuer1": {"spirex@example.com"}, + }, + }, + status: corev1.ContainerStatus{ + Image: "spire-agent-sigstore-1", + ImageID: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + ContainerID: "000000", + }, + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: []string{ + "000000:image-signature-subject:spirex@example.com", "000000:image-signature-content:MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIgdgBP96BO1Te/NdbjHbUeb0BUye6deRgVtQEv5No5smA=", "000000:image-signature-logid:samplelogID", "000000:image-signature-integrated-time:12345", "sigstore-validation:passed", + }, + }, + { + name: "Attest skipped image", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createNilVerifyFunction(), + fetchBinding: createNilFetchFunction(), + checkOptsBinding: createNilCheckOptsFunction(), + }, + skippedImages: map[string]struct{}{ + "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505": {}, + }, + rekorURL: rekorDefaultURL(), + }, + status: corev1.ContainerStatus{ + Image: "spire-agent-sigstore-2", + ImageID: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + ContainerID: "111111", + }, + want: []string{ + "sigstore-validation:passed", + }, + }, + { + name: "Attest image with no signature", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createVerifyFunction(nil, true, fmt.Errorf("no signature found")), + fetchBinding: createFetchFunction(&remote.Descriptor{ + Manifest: []byte("sometext"), + }, nil), + checkOptsBinding: createCheckOptsFunction(defaultCheckOpts, nil), + }, + rekorURL: rekorDefaultURL(), + }, + status: corev1.ContainerStatus{ + Image: "spire-agent-sigstore-3", + ImageID: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + ContainerID: "222222", + }, + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedVerifyArguments: verifyFunctionArguments{ + context: context.Background(), + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: defaultCheckOpts, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: rekorDefaultURL(), + }, + want: nil, + wantedErr: fmt.Errorf("error verifying signature: %w", errors.New("no signature found")), + }, + { + name: "Attest image with empty rekorURL", + fields: fields{ + functionBindings: sigstoreFunctionBindings{ + verifyBinding: createNilVerifyFunction(), + fetchBinding: createFetchFunction(&remote.Descriptor{ + Manifest: []byte("sometext"), + }, nil), + checkOptsBinding: createCheckOptsFunction(emptyURLCheckOpts, emptyError), + }, + rekorURL: url.URL{}, + }, + status: corev1.ContainerStatus{ + Image: "spire-agent-sigstore-3", + ImageID: "docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505", + ContainerID: "222222", + }, + wantedFetchArguments: fetchFunctionArguments{ + ref: name.MustParseReference("docker-registry.com/some/image@sha256:5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505"), + options: nil, + }, + wantedCheckOptsArguments: checkOptsFunctionArguments{ + url: url.URL{}, + }, + want: nil, + wantedErr: fmt.Errorf("could not create cosign check options: %w", emptyError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fetchArguments := fetchFunctionArguments{} + verifyArguments := verifyFunctionArguments{} + checkOptsArguments := checkOptsFunctionArguments{} + sigstore := &sigstoreImpl{ + functionHooks: sigstoreFunctionHooks{ + verifyFunction: tt.fields.functionBindings.verifyBinding(t, &verifyArguments), + fetchImageManifestFunction: tt.fields.functionBindings.fetchBinding(t, &fetchArguments), + checkOptsFunction: tt.fields.functionBindings.checkOptsBinding(t, &checkOptsArguments), + }, + skippedImages: tt.fields.skippedImages, + rekorURL: tt.fields.rekorURL, + sigstorecache: NewCache(maximumAmountCache), + logger: hclog.Default(), + } + + for issuer, subjects := range tt.fields.subjectAllowList { + for _, subject := range subjects { + sigstore.AddAllowedSubject(issuer, subject) + } + } + + got, err := sigstore.AttestContainerSignatures(context.Background(), &tt.status) + + if tt.wantedErr != nil { + require.EqualError(t, err, tt.wantedErr.Error()) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.want, got) + require.Equal(t, tt.wantedFetchArguments, fetchArguments) + require.Equal(t, tt.wantedVerifyArguments, verifyArguments) + require.Equal(t, tt.wantedCheckOptsArguments, checkOptsArguments) + }) + } +} + +func TestSigstoreimpl_SetRekorURL(t *testing.T) { + tests := []struct { + name string + rekorURL url.URL + rekorURLArg string + want url.URL + wantedErr error + }{ + { + name: "SetRekorURL", + rekorURL: url.URL{}, + rekorURLArg: "https://rekor.com", + want: url.URL{ + Scheme: "https", + Host: "rekor.com", + }, + }, + { + name: "SetRekorURL with empty url", + rekorURL: url.URL{ + Scheme: "https", + Host: "non.empty.url", + }, + rekorURLArg: "", + want: url.URL{ + Scheme: "https", + Host: "non.empty.url", + }, + wantedErr: fmt.Errorf("rekor URL is empty"), + }, + { + name: "SetRekorURL with invalid URL", + rekorURL: url.URL{}, + rekorURLArg: "http://invalid.{{}))}.url.com", // invalid url + want: url.URL{}, + wantedErr: fmt.Errorf("failed parsing rekor URI: parse %q: invalid character %q in host name", "http://invalid.{{}))}.url.com", "{"), + }, + { + name: "SetRekorURL with empty host url", + rekorURL: url.URL{}, + rekorURLArg: "path-no-host", // URI parser uses this as path, not host + want: url.URL{}, + wantedErr: fmt.Errorf("host is required on rekor URL"), + }, + { + name: "SetRekorURL with invalid URL scheme", + rekorURL: url.URL{}, + rekorURLArg: "abc://invalid.scheme.com", // invalid scheme + want: url.URL{}, + wantedErr: fmt.Errorf("invalid rekor URL Scheme %q", "abc"), + }, + { + name: "SetRekorURL with empty URL scheme", + rekorURL: url.URL{}, + rekorURLArg: "//no.scheme.com/path", // empty scheme + want: url.URL{}, + wantedErr: fmt.Errorf("invalid rekor URL Scheme \"\""), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sigstore := &sigstoreImpl{ + rekorURL: tt.rekorURL, + } + err := sigstore.SetRekorURL(tt.rekorURLArg) + if tt.wantedErr != nil { + require.EqualError(t, err, tt.wantedErr.Error()) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.want, sigstore.rekorURL) + }) + } +} + +type signature struct { + oci.Signature + + payload []byte + cert *x509.Certificate + bundle *bundle.RekorBundle +} + +func (s signature) Payload() ([]byte, error) { + return s.payload, nil +} + +func (s signature) Cert() (*x509.Certificate, error) { + return s.cert, nil +} + +func (s signature) Bundle() (*bundle.RekorBundle, error) { + return s.bundle, nil +} + +type noPayloadSignature signature + +func (noPayloadSignature) Payload() ([]byte, error) { + return nil, errors.New("no payload test") +} + +type nilBundleSignature signature + +func (s nilBundleSignature) Payload() ([]byte, error) { + return s.payload, nil +} + +func (s nilBundleSignature) Cert() (*x509.Certificate, error) { + return s.cert, nil +} + +func (s nilBundleSignature) Bundle() (*bundle.RekorBundle, error) { + return nil, fmt.Errorf("no bundle test") +} + +type noCertSignature signature + +func (s noCertSignature) Payload() ([]byte, error) { + return s.payload, nil +} + +func (noCertSignature) Cert() (*x509.Certificate, error) { + return nil, errors.New("no cert test") +} + +func createVerifyFunction(returnSignatures []oci.Signature, returnBundleVerified bool, returnError error) verifyFunctionBinding { + bindVerifyArgumentsFunction := func(t require.TestingT, verifyArguments *verifyFunctionArguments) verifyFunctionType { + newVerifyFunction := func(context context.Context, ref name.Reference, co *cosign.CheckOpts) ([]oci.Signature, bool, error) { + verifyArguments.context = context + verifyArguments.ref = ref + verifyArguments.options = co + return returnSignatures, returnBundleVerified, returnError + } + return newVerifyFunction + } + return bindVerifyArgumentsFunction +} + +func createNilVerifyFunction() verifyFunctionBinding { + bindVerifyArgumentsFunction := func(t require.TestingT, verifyArguments *verifyFunctionArguments) verifyFunctionType { + failFunction := func(context context.Context, ref name.Reference, co *cosign.CheckOpts) ([]oci.Signature, bool, error) { + require.FailNow(t, "nil verify function should not be called") + return nil, false, nil + } + return failFunction + } + return bindVerifyArgumentsFunction +} + +func createFetchFunction(returnDescriptor *remote.Descriptor, returnError error) fetchFunctionBinding { + bindFetchArgumentsFunction := func(t require.TestingT, fetchArguments *fetchFunctionArguments) fetchImageManifestFunctionType { + newFetchFunction := func(ref name.Reference, options ...remote.Option) (*remote.Descriptor, error) { + fetchArguments.ref = ref + fetchArguments.options = options + return returnDescriptor, returnError + } + return newFetchFunction + } + return bindFetchArgumentsFunction +} + +func createNilFetchFunction() fetchFunctionBinding { + bindFetchArgumentsFunction := func(t require.TestingT, fetchArguments *fetchFunctionArguments) fetchImageManifestFunctionType { + failFunction := func(ref name.Reference, options ...remote.Option) (*remote.Descriptor, error) { + require.FailNow(t, "nil fetch function should not be called") + return nil, nil + } + return failFunction + } + return bindFetchArgumentsFunction +} + +func createCheckOptsFunction(returnCheckOpts *cosign.CheckOpts, returnErr error) checkOptsFunctionBinding { + bindCheckOptsArgumentsFunction := func(t require.TestingT, checkOptsArguments *checkOptsFunctionArguments) checkOptsFunctionType { + newCheckOptsFunction := func(url url.URL, enforceSCT bool) (*cosign.CheckOpts, error) { + checkOptsArguments.url = url + return returnCheckOpts, returnErr + } + return newCheckOptsFunction + } + return bindCheckOptsArgumentsFunction +} + +func createNilCheckOptsFunction() checkOptsFunctionBinding { + bindCheckOptsArgumentsFunction := func(t require.TestingT, checkOptsArguments *checkOptsFunctionArguments) checkOptsFunctionType { + failFunction := func(url url.URL, enforceSCT bool) (*cosign.CheckOpts, error) { + require.FailNow(t, "nil check opts function should not be called") + return nil, fmt.Errorf("nil check opts function should not be called") + } + return failFunction + } + return bindCheckOptsArgumentsFunction +} + +func rekorDefaultURL() url.URL { + return url.URL{ + Scheme: rekor.DefaultSchemes[0], + Host: rekor.DefaultHost, + Path: rekor.DefaultBasePath, + } +} + +type sigstoreFunctionBindings struct { + verifyBinding verifyFunctionBinding + fetchBinding fetchFunctionBinding + checkOptsBinding checkOptsFunctionBinding +} + +type checkOptsFunctionArguments struct { + url url.URL +} + +type checkOptsFunctionBinding func(require.TestingT, *checkOptsFunctionArguments) checkOptsFunctionType + +type fetchFunctionArguments struct { + ref name.Reference + options []remote.Option +} + +type fetchFunctionBinding func(require.TestingT, *fetchFunctionArguments) fetchImageManifestFunctionType + +type verifyFunctionArguments struct { + context context.Context + ref name.Reference + options *cosign.CheckOpts +} + +type verifyFunctionBinding func(require.TestingT, *verifyFunctionArguments) verifyFunctionType diff --git a/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstorecache.go b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstorecache.go new file mode 100644 index 0000000000..4d556a679d --- /dev/null +++ b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstorecache.go @@ -0,0 +1,88 @@ +//go:build !windows +// +build !windows + +package sigstore + +import ( + "container/list" + "sync" +) + +// Item represents a key-value pair +type Item struct { + Key string + Value []SelectorsFromSignatures +} + +// Cache defines the behaviors of our cache +type Cache interface { + GetSignature(key string) *Item + PutSignature(Item) +} + +// Map for signatures is created +type MapItem struct { + element *list.Element + item *Item +} + +// cacheImpl implements Cache interface +type cacheImpl struct { + size int + items *list.List + mutex sync.RWMutex + itemsMap map[string]MapItem +} + +// NewCache creates and returns a new cache +func NewCache(maximumAmountCache int) Cache { + return &cacheImpl{ + size: maximumAmountCache, + items: list.New(), + mutex: sync.RWMutex{}, + itemsMap: make(map[string]MapItem), + } +} + +// GetSignature returns an existing item from the cache. +// Get also moves the existing item to the front of the items list to indicate that the existing item is recently used. +func (c *cacheImpl) GetSignature(key string) *Item { + c.mutex.RLock() + defer c.mutex.RUnlock() + + e, ok := c.itemsMap[key] + if !ok { + return nil + } + + c.items.MoveToFront(e.element) + + return e.item +} + +// PutSignature puts a new item into the cache. +// Put removes the least recently used item from the items list when the cache is full. +// Put pushes the new item to the front of the items list to indicate that the new item is recently used. +func (c *cacheImpl) PutSignature(i Item) { + c.mutex.Lock() + defer c.mutex.Unlock() + + e, ok := c.itemsMap[i.Key] + if ok { + c.items.Remove(e.element) + c.itemsMap[i.Key] = MapItem{ + element: c.items.PushFront(i.Key), + item: &i, + } + return + } + if c.items.Len() >= c.size { + removed := c.items.Remove(c.items.Back()) + delete(c.itemsMap, removed.(string)) + } + + c.itemsMap[i.Key] = MapItem{ + element: c.items.PushFront(i.Key), + item: &i, + } +} diff --git a/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstorecache_test.go b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstorecache_test.go new file mode 100644 index 0000000000..cf50ef6aeb --- /dev/null +++ b/pkg/agent/plugin/workloadattestor/k8s/sigstore/sigstorecache_test.go @@ -0,0 +1,542 @@ +//go:build !windows +// +build !windows + +package sigstore + +import ( + "container/list" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + selectors1 = Item{ + Key: "signature1", + Value: []SelectorsFromSignatures{ + { + Subject: "spirex1@example.com", + Content: "content1", + LogID: "1111111111111111", + IntegratedTime: "1111111111111111", + }, + }, + } + + selectors2 = Item{ + Key: "signature2", + Value: []SelectorsFromSignatures{ + { + Subject: "spirex2@example.com", + Content: "content2", + LogID: "2222222222222222", + IntegratedTime: "2222222222222222", + }, + }, + } + + selectors3 = Item{ + Key: "signature3", + Value: []SelectorsFromSignatures{ + { + Subject: "spirex3@example.com", + Content: "content3", + LogID: "3333333333333333", + IntegratedTime: "3333333333333333", + }, + }, + } + + selectors3Updated = Item{ + Key: "signature3", + Value: []SelectorsFromSignatures{ + { + Subject: "spirex3@example.com", + Content: "content4", + LogID: "4444444444444444", + IntegratedTime: "4444444444444444", + }, + }, + } + + selectors2Updated = Item{ + Key: "signature2", + Value: []SelectorsFromSignatures{ + { + Subject: "spirex2@example.com", + Content: "content5", + LogID: "5555555555555555", + IntegratedTime: "5555555555555555", + }, + }, + } +) + +func TestNewCache(t *testing.T) { + want := &cacheImpl{ + size: 3, + items: list.New(), + mutex: sync.RWMutex{}, + itemsMap: make(map[string]MapItem), + } + got := NewCache(3) + require.Equal(t, want, got, "NewCache() = %v, want %v", got, want) +} + +func TestCacheimpl_GetSignature(t *testing.T) { + m, items := makeMapAndList(&selectors1, &selectors2) + cacheInstance := &cacheImpl{ + size: 3, + items: items, + mutex: sync.RWMutex{}, + itemsMap: m, + } + + caseInOrderList := list.New() + caseInOrderList.PushFront(selectors1.Key) + caseInOrderList.PushFront(selectors2.Key) + + caseReorderList := list.New() + caseReorderList.PushFront(selectors2.Key) + caseReorderList.PushFront(selectors1.Key) + + tests := []struct { + name string + want *Item + key string + errorMessage string + wantedMap map[string]MapItem + wantedList *list.List + }{ + { + name: "Non existing entry", + want: nil, + key: selectors3.Key, + errorMessage: "A non-existing item's key should return a nil item.", + wantedMap: map[string]MapItem{ + "signature1": {item: &selectors1, element: m[selectors1.Key].element}, + "signature2": {item: &selectors2, element: m[selectors2.Key].element}, + }, + wantedList: caseInOrderList, + }, + { + name: "Existing entry", + want: &selectors2, + key: selectors2.Key, + wantedMap: map[string]MapItem{ + "signature1": {item: &selectors1, element: m[selectors1.Key].element}, + "signature2": {item: &selectors2, element: m[selectors2.Key].element}, + }, + wantedList: caseInOrderList, + }, + { + name: "Existing entry, reorder on get", + want: &selectors1, + key: selectors1.Key, + wantedMap: map[string]MapItem{ + "signature1": {item: &selectors1, element: m[selectors1.Key].element}, + "signature2": {item: &selectors2, element: m[selectors2.Key].element}, + }, + wantedList: caseReorderList, + }, + } + + for _, tt := range tests { + got := cacheInstance.GetSignature(tt.key) + require.Equal(t, got, tt.want, "%v Got: %v Want: %v", tt.errorMessage, got, tt.want) + require.Equal(t, tt.wantedList, cacheInstance.items, "Lists are different Got: %v Want: %v", cacheInstance.items, tt.wantedList) + require.Equal(t, tt.wantedMap, cacheInstance.itemsMap, "Maps are different Got: %v Want: %v", cacheInstance.itemsMap, tt.wantedMap) + } +} + +func TestCacheimpl_PutSignature(t *testing.T) { + mapReorder, listReorder := makeMapAndList(&selectors2, &selectors3) + mapAddNew, listAddNew := makeMapAndList(&selectors3, &selectors2, &selectors1) + mapUpdate, listUpdate := makeMapAndList(&selectors3, &selectors2Updated) + mapReorderUpdate, listReorderUpdate := makeMapAndList(&selectors2, &selectors3Updated) + tests := []struct { + name string + item *Item + wantLength int + wantKey string + wantValue *Item + wantMap map[string]MapItem + wantList *list.List + }{ + { + name: "Put existing element", + item: &selectors3, + wantLength: 2, + wantKey: selectors3.Key, + wantValue: &selectors3, + wantMap: mapReorder, + wantList: listReorder, + }, + { + name: "Put new element", + item: &selectors1, + wantLength: 3, + wantKey: selectors1.Key, + wantValue: &selectors1, + wantMap: mapAddNew, + wantList: listAddNew, + }, + { + name: "Update entry", + item: &selectors2Updated, + wantLength: 2, + wantKey: selectors2.Key, + wantValue: &selectors2Updated, + wantMap: mapUpdate, + wantList: listUpdate, + }, + { + name: "Update entry, reorder", + item: &selectors3Updated, + wantLength: 2, + wantKey: selectors3.Key, + wantValue: &selectors3Updated, + wantMap: mapReorderUpdate, + wantList: listReorderUpdate, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + testMap, testItems := makeMapAndList(&selectors3, &selectors2) + cacheInstance := cacheImpl{ + size: 10, + items: testItems, + mutex: sync.RWMutex{}, + itemsMap: testMap, + } + cacheInstance.PutSignature(*tt.item) + + gotItem, present := testMap[tt.wantKey] + require.True(t, present, "key not found") + require.Equal(t, tt.wantValue, gotItem.item, "Value different than expected. \nGot: %v \nWant:%v", gotItem.item, tt.wantValue) + require.Equal(t, tt.wantLength, testItems.Len(), "List length different than expected. \nGot: %v \nWant:%v", testItems.Len(), tt.wantLength) + require.Equal(t, tt.wantList, testItems, "Lists are different Got: %v Want: %v", testItems, tt.wantList) + require.Equal(t, tt.wantMap, testMap, "Maps are different Got: %v Want: %v", testMap, tt.wantMap) + }) + } +} + +func TestCacheimpl_CheckOverflowAndUpdates(t *testing.T) { + m := make(map[string]MapItem) + items := list.New() + + cacheInstance := &cacheImpl{ + size: 2, + items: items, + mutex: sync.RWMutex{}, + itemsMap: m, + } + + putSteps1 := []struct { + name string + item *Item + wantLength int + wantKey string + wantValue *Item + wantHeadKey string + }{ + { + name: "Put first element", + item: &selectors1, + wantLength: 1, + wantKey: selectors1.Key, + wantValue: &selectors1, + wantHeadKey: selectors1.Key, + }, + { + name: "Put first element again", + item: &selectors1, + wantLength: 1, + wantKey: selectors1.Key, + wantValue: &selectors1, + wantHeadKey: selectors1.Key, + }, + { + name: "Put second element", + item: &selectors2, + wantLength: 2, + wantKey: selectors2.Key, + wantValue: &selectors2, + wantHeadKey: selectors2.Key, + }, + { + name: "Put third element, Overflow cache", + item: &selectors3, + wantLength: 2, + wantKey: selectors3.Key, + wantValue: &selectors3, + wantHeadKey: selectors3.Key, + }, + { + name: "Update entry", + item: &selectors3Updated, + wantLength: 2, + wantKey: selectors3.Key, + wantValue: &selectors3Updated, + wantHeadKey: selectors3.Key, + }, + { + name: "Put second element, again", + item: &selectors2, + wantLength: 2, + wantKey: selectors2.Key, + wantValue: &selectors2, + wantHeadKey: selectors2.Key, + }, + } + getSteps1 := []struct { + name string + key string + item *Item + wantLength int + wantValue *Item + wantHeadKey string + }{ + { + name: "Get first element", + key: selectors1.Key, + item: nil, + wantLength: 2, + wantHeadKey: selectors2.Key, + }, + { + name: "Get third element", + key: selectors3.Key, + item: &selectors3Updated, + wantLength: 2, + wantHeadKey: selectors3.Key, + }, + { + name: "Get first element, after third element was accessed", + key: selectors1.Key, + item: nil, + wantLength: 2, + wantHeadKey: selectors3.Key, + }, + { + name: "Get second element", + key: selectors2.Key, + item: &selectors2, + wantLength: 2, + wantValue: &selectors2, + wantHeadKey: selectors2.Key, + }, + } + + putSteps2 := []struct { + name string + item *Item + wantLength int + wantKey string + wantValue *Item + wantHeadKey string + }{ + { + name: "Put first element again, overflow cache", + item: &selectors1, + wantLength: 2, + wantKey: selectors1.Key, + wantValue: &selectors1, + wantHeadKey: selectors1.Key, + }, + { + name: "Put second element updated", + item: &selectors2Updated, + wantLength: 2, + wantKey: selectors2.Key, + wantValue: &selectors2Updated, + wantHeadKey: selectors2.Key, + }, + { + name: "Put third element again, overflow cache", + item: &selectors3Updated, + wantLength: 2, + wantKey: selectors3.Key, + wantValue: &selectors3Updated, + wantHeadKey: selectors3.Key, + }, + { + name: "Revert third entry", + item: &selectors3, + wantLength: 2, + wantKey: selectors3.Key, + wantValue: &selectors3, + wantHeadKey: selectors3.Key, + }, + { + name: "Pull second element to front", + item: &selectors2Updated, + wantLength: 2, + wantKey: selectors2.Key, + wantValue: &selectors2Updated, + wantHeadKey: selectors2.Key, + }, + { + name: "Put first element for the last time, overflow cache", + item: &selectors1, + wantLength: 2, + wantKey: selectors1.Key, + wantValue: &selectors1, + wantHeadKey: selectors1.Key, + }, + } + + getSteps2 := []struct { + name string + key string + item *Item + wantLength int + wantValue *Item + wantHeadKey string + }{ + { + name: "Get third element, should fail", + key: selectors3.Key, + item: nil, + wantLength: 2, + wantHeadKey: selectors1.Key, + }, + { + name: "Get third element again, should not change head", + key: selectors3.Key, + item: nil, + wantLength: 2, + wantHeadKey: selectors1.Key, + }, + { + name: "Get first element", + key: selectors1.Key, + item: &selectors1, + wantLength: 2, + wantHeadKey: selectors1.Key, + }, + { + name: "Get second element", + key: selectors2.Key, + item: &selectors2Updated, + wantLength: 2, + wantHeadKey: selectors2.Key, + }, + { + name: "Get third element again, should have new head from last get", + key: selectors3.Key, + item: nil, + wantLength: 2, + wantHeadKey: selectors2.Key, + }, + } + + for _, step := range putSteps1 { + cacheInstance.PutSignature(*step.item) + require.Contains(t, m, step.wantKey, "Key %q should be in the map after step %q", step.wantKey, step.name) + gotItem := m[step.wantKey].item + + require.Equal(t, gotItem, step.wantValue, "Value different than expected. \nGot: %v \nWant:%v", gotItem, step.wantValue) + require.Equal(t, items.Len(), step.wantLength, "Item count should be %v after step %q", step.wantLength, step.name) + require.Equal(t, items.Front().Value, step.wantHeadKey, "First element is %v should be %v after step %q", items.Front().Value, step.wantHeadKey, step.name) + } + for _, step := range getSteps1 { + gotItem := cacheInstance.GetSignature(step.key) + + require.Equal(t, gotItem, step.item, "Value different than expected. \nGot: %v \nWant:%v", gotItem, step.item) + require.Equal(t, items.Len(), step.wantLength, "Item count should be %v after step %q", step.wantLength, step.name) + require.Equal(t, items.Front().Value, step.wantHeadKey, "First element is %v should be %v after step %q", items.Front().Value, step.wantHeadKey, step.name) + } + for _, step := range putSteps2 { + cacheInstance.PutSignature(*step.item) + require.Contains(t, m, step.wantKey, "Key %q should be in the map after step %q", step.wantKey, step.name) + gotItem := m[step.wantKey].item + + require.Equal(t, gotItem, step.wantValue, "Value different than expected. \nGot: %v \nWant:%v", gotItem, step.wantValue) + require.Equal(t, items.Len(), step.wantLength, "Item count should be %v after step %q", step.wantLength, step.name) + require.Equal(t, items.Front().Value, step.wantHeadKey, "First element is %v should be %v after step %q", items.Front().Value, step.wantHeadKey, step.name) + } + for _, step := range getSteps2 { + gotItem := cacheInstance.GetSignature(step.key) + + require.Equal(t, gotItem, step.item, "Value different than expected. \nGot: %v \nWant:%v", gotItem, step.item) + require.Equal(t, items.Len(), step.wantLength, "Item count should be %v after step %q", step.wantLength, step.name) + require.Equal(t, items.Front().Value, step.wantHeadKey, "First element is %v should be %v after step %q", items.Front().Value, step.wantHeadKey, step.name) + } +} + +func TestCacheimpl_CheckOverflow(t *testing.T) { + mapNoOverflow, listNoOverflow := makeMapAndList(&selectors1, &selectors2, &selectors3) + mapOverflow, listOverflow := makeMapAndList(&selectors2, &selectors3) + mapReorder, listReorder := makeMapAndList(&selectors2, &selectors1) + + testCases := []struct { + name string + item *Item + wantLength int + wantedList *list.List + wantedMap map[string]MapItem + maxLength int + }{ + { + name: "Put third element, no overflow", + item: &selectors3, + wantedList: listNoOverflow, + wantedMap: mapNoOverflow, + maxLength: 3, + }, + { + name: "Put existing element no overflow", + item: &selectors1, + wantedList: listReorder, + wantedMap: mapReorder, + maxLength: 2, + }, + { + name: "Put third element, overflow", + item: &selectors3, + wantedList: listOverflow, + wantedMap: mapOverflow, + maxLength: 2, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + m := make(map[string]MapItem) + items := list.New() + m[selectors1.Key] = MapItem{ + item: &selectors1, + element: items.PushFront(selectors1.Key), + } + m[selectors2.Key] = MapItem{ + item: &selectors2, + element: items.PushFront(selectors2.Key), + } + cacheInstance := &cacheImpl{ + size: testCase.maxLength, + items: items, + mutex: sync.RWMutex{}, + itemsMap: m, + } + cacheInstance.PutSignature(*testCase.item) + require.Equal(t, testCase.wantedList, cacheInstance.items, "List different than expected. \nGot: %v \nWant:%v", cacheInstance.items, testCase.wantedList) + require.Equal(t, testCase.wantedMap, cacheInstance.itemsMap, "Map different than expected. \nGot: %v \nWant:%v", cacheInstance.itemsMap, testCase.wantedMap) + }) + } +} + +func makeMapAndList(items ...*Item) (map[string]MapItem, *list.List) { + mp := make(map[string]MapItem) + ls := list.New() + for _, item := range items { + mp[item.Key] = MapItem{ + item: item, + element: ls.PushFront(item.Key), + } + } + return mp, ls +} diff --git a/pkg/agent/storage/legacy.go b/pkg/agent/storage/legacy.go index 18a7487ac4..acf1d863bd 100644 --- a/pkg/agent/storage/legacy.go +++ b/pkg/agent/storage/legacy.go @@ -31,7 +31,7 @@ func storeLegacyBundle(dir string, bundle []*x509.Certificate) error { for _, cert := range bundle { data.Write(cert.Raw) } - if err := diskutil.AtomicWriteFile(legacyBundlePath(dir), data.Bytes(), 0600); err != nil { + if err := diskutil.AtomicWritePrivateFile(legacyBundlePath(dir), data.Bytes()); err != nil { return fmt.Errorf("failed to store legacy bundle: %w", err) } return nil @@ -55,7 +55,7 @@ func storeLegacySVID(dir string, svidChain []*x509.Certificate) error { for _, cert := range svidChain { data.Write(cert.Raw) } - if err := diskutil.AtomicWriteFile(legacySVIDPath(dir), data.Bytes(), 0600); err != nil { + if err := diskutil.AtomicWritePrivateFile(legacySVIDPath(dir), data.Bytes()); err != nil { return fmt.Errorf("failed to store legacy SVID: %w", err) } return nil diff --git a/pkg/agent/storage/storage.go b/pkg/agent/storage/storage.go index e1f26886cb..4498d2e1d4 100644 --- a/pkg/agent/storage/storage.go +++ b/pkg/agent/storage/storage.go @@ -244,7 +244,7 @@ func storeData(dir string, data storageData) error { return fmt.Errorf("failed to marshal data: %w", err) } - if err := diskutil.AtomicWriteFile(path, marshaled, 0600); err != nil { + if err := diskutil.AtomicWritePrivateFile(path, marshaled); err != nil { return fmt.Errorf("failed to write data file: %w", err) } diff --git a/pkg/common/cliprinter/cliprinter.go b/pkg/common/cliprinter/cliprinter.go index 272613005c..bee58dad4a 100644 --- a/pkg/common/cliprinter/cliprinter.go +++ b/pkg/common/cliprinter/cliprinter.go @@ -1,9 +1,10 @@ package cliprinter import ( + "errors" "io" - "os" + commoncli "github.com/spiffe/spire/pkg/common/cli" "github.com/spiffe/spire/pkg/common/cliprinter/internal/errorjson" "github.com/spiffe/spire/pkg/common/cliprinter/internal/errorpretty" "github.com/spiffe/spire/pkg/common/cliprinter/internal/protojson" @@ -16,9 +17,9 @@ import ( // Printer is an interface for providing a printer implementation to // a CLI utility. type Printer interface { - MustPrintError(error) - MustPrintProto(...proto.Message) - MustPrintStruct(...interface{}) + PrintError(error) error + PrintProto(...proto.Message) error + PrintStruct(...interface{}) error } // CustomPrettyFunc is used to provide a custom function for pretty @@ -26,77 +27,66 @@ type Printer interface { // for pre-existing CLI code, such that this code can supply a // custom pretty printer that mirrors its current behavior, but // still be able to gain formatter functionality for other outputs. -type CustomPrettyFunc func(...interface{}) error +type CustomPrettyFunc func(*commoncli.Env, ...interface{}) error + +// ErrInternalCustomPrettyFunc should be returned by a CustomPrettyFunc when some internal error occurs. +var ErrInternalCustomPrettyFunc = errors.New("internal error: cli printer; please report this bug") type printer struct { format formatType - - stdout io.Writer - stderr io.Writer - - cp CustomPrettyFunc -} - -func newPrinter(f formatType) *printer { - return newPrinterWithWriters(f, os.Stdout, os.Stderr) + env *commoncli.Env + cp CustomPrettyFunc } -func newPrinterWithWriters(f formatType, stdout, stderr io.Writer) *printer { +func newPrinter(f formatType, env *commoncli.Env) *printer { + if env == nil { + env = commoncli.DefaultEnv + } return &printer{ format: f, - stdout: stdout, - stderr: stderr, + env: env, } } -// MustPrintError prints an error and applies the configured formatting. If -// an error is encountered while printing, MustPrintError will call os.Exit(2). -func (p *printer) MustPrintError(err error) { - if err := p.printError(err); err != nil { - os.Exit(2) - } +// PrintError prints an error and applies the configured formatting. +func (p *printer) PrintError(err error) error { + return p.printError(err) } -// PrintProto prints a protobuf message and applies the configured formatting. If -// an error is encountered while printing, MustPrintProto will call os.Exit(2). -func (p *printer) MustPrintProto(msg ...proto.Message) { - if err := p.printProto(msg...); err != nil { - os.Exit(2) - } +// PrintProto prints a protobuf message and applies the configured formatting. +func (p *printer) PrintProto(msg ...proto.Message) error { + return p.printProto(msg...) } -// PrintStruct prints a struct and applies the configured formatting. If -// an error is encountered while printing, MustPrintStruct will call os.Exit(2). -func (p *printer) MustPrintStruct(msg ...interface{}) { - if err := p.printStruct(msg); err != nil { - os.Exit(2) - } +// PrintStruct prints a struct and applies the configured formatting. +func (p *printer) PrintStruct(msg ...interface{}) error { + return p.printStruct(msg) } func (p *printer) printError(err error) error { switch p.format { case json: - return errorjson.Print(err, p.stdout, p.stderr) + return errorjson.Print(err, p.env.Stdout, p.env.Stderr) default: - return p.printPrettyError(err, p.stdout, p.stderr) + return p.printPrettyError(err, p.env.Stdout, p.env.Stderr) } } func (p *printer) printProto(msg ...proto.Message) error { switch p.format { case json: - return protojson.Print(msg, p.stdout, p.stderr) + return protojson.Print(msg, p.env.Stdout, p.env.Stderr) default: - return p.printPrettyProto(msg, p.stdout, p.stderr) + return p.printPrettyProto(msg, p.env.Stdout, p.env.Stderr) } } func (p *printer) printStruct(msg ...interface{}) error { switch p.format { case json: - return structjson.Print(msg, p.stdout, p.stderr) + return structjson.Print(msg, p.env.Stdout, p.env.Stderr) default: - return p.printPrettyStruct(msg, p.stdout, p.stderr) + return p.printPrettyStruct(msg, p.env.Stdout, p.env.Stderr) } } @@ -110,7 +100,7 @@ func (p *printer) setCustomPrettyPrinter(cp CustomPrettyFunc) { func (p *printer) printPrettyError(err error, stdout, stderr io.Writer) error { if p.cp != nil { - return p.cp(err) + return p.cp(p.env, err) } return errorpretty.Print(err, stdout, stderr) @@ -122,14 +112,14 @@ func (p *printer) printPrettyProto(msgs []proto.Message, stdout, stderr io.Write m = append(m, msg.(interface{})) } - return p.cp(m...) + return p.cp(p.env, m...) } return protopretty.Print(msgs, stdout, stderr) } func (p *printer) printPrettyStruct(msg []interface{}, stdout, stderr io.Writer) error { if p.cp != nil { - return p.cp(msg...) + return p.cp(p.env, msg...) } return structpretty.Print(msg, stdout, stderr) diff --git a/pkg/common/cliprinter/cliprinter_test.go b/pkg/common/cliprinter/cliprinter_test.go index 77b3142485..55576240c9 100644 --- a/pkg/common/cliprinter/cliprinter_test.go +++ b/pkg/common/cliprinter/cliprinter_test.go @@ -7,6 +7,7 @@ import ( "testing" agentapi "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" + commoncli "github.com/spiffe/spire/pkg/common/cli" ) func TestPrintError(t *testing.T) { @@ -97,8 +98,12 @@ func newTestPrinterWithWriter(stdout, stderr io.Writer) *printer { if stderr == nil { stderr = new(bytes.Buffer) } + env := &commoncli.Env{ + Stdout: stdout, + Stderr: stderr, + } - return newPrinterWithWriters(defaultFormatType, stdout, stderr) + return newPrinter(defaultFormatType, env) } type badWriter struct{} diff --git a/pkg/common/cliprinter/flag.go b/pkg/common/cliprinter/flag.go index 7f90834885..fc4cf741b1 100644 --- a/pkg/common/cliprinter/flag.go +++ b/pkg/common/cliprinter/flag.go @@ -4,12 +4,23 @@ import ( "errors" "flag" "fmt" + + commoncli "github.com/spiffe/spire/pkg/common/cli" +) + +const defaultFlagName = "output" + +var flagDescription = fmt.Sprintf( + "Desired output format (%s, %s); default: %s.", + formatTypeToStr(pretty), + formatTypeToStr(json), + formatTypeToStr(defaultFormatType), ) // AppendFlag adds the -format flag to the provided flagset, and populates // the referenced Printer interface with a properly configured printer. -func AppendFlag(p *Printer, fs *flag.FlagSet) { - AppendFlagWithCustomPretty(p, fs, nil) +func AppendFlag(p *Printer, fs *flag.FlagSet, env *commoncli.Env) *FormatterFlag { + return AppendFlagWithCustomPretty(p, fs, env, nil) } // AppendFlagWithCustomPretty is the same as AppendFlag, however it also allows @@ -17,19 +28,21 @@ func AppendFlag(p *Printer, fs *flag.FlagSet) { // to override the pretty print logic that normally ships with this package. Its // intended use is to allow for the adoption of cliprinter while still retaining // backwards compatibility with the legacy/bespoke pretty print output. -func AppendFlagWithCustomPretty(p *Printer, fs *flag.FlagSet, cp CustomPrettyFunc) { +func AppendFlagWithCustomPretty(p *Printer, fs *flag.FlagSet, env *commoncli.Env, cp CustomPrettyFunc) *FormatterFlag { // Set the default - np := newPrinter(defaultFormatType) + np := newPrinter(defaultFormatType, env) np.setCustomPrettyPrinter(cp) *p = np f := &FormatterFlag{ p: p, f: defaultFormatType, + env: env, customPretty: cp, } - fs.Var(f, "format", "Desired output format (pretty, json)") + fs.Var(f, defaultFlagName, flagDescription) + return f } type FormatterFlag struct { @@ -37,8 +50,10 @@ type FormatterFlag struct { // A pointer to our consumer's Printer interface, along with // its format type - p *Printer - f formatType + p *Printer + f formatType + env *commoncli.Env + isSet bool } func (f *FormatterFlag) String() string { @@ -50,6 +65,9 @@ func (f *FormatterFlag) String() string { } func (f *FormatterFlag) Set(formatStr string) error { + if f.isSet && formatTypeToStr(f.f) != formatStr { + return fmt.Errorf("the output format has already been set to %q", formatTypeToStr(f.f)) + } if f.p == nil { return errors.New("internal error: formatter flag not correctly invoked; please report this bug") } @@ -59,10 +77,11 @@ func (f *FormatterFlag) Set(formatStr string) error { return fmt.Errorf("bad formatter flag: %w", err) } - np := newPrinter(format) + np := newPrinter(format, f.env) np.setCustomPrettyPrinter(f.customPretty) *f.p = np f.f = format + f.isSet = true return nil } diff --git a/pkg/common/cliprinter/flag_test.go b/pkg/common/cliprinter/flag_test.go index 737e078f65..5f4471addf 100644 --- a/pkg/common/cliprinter/flag_test.go +++ b/pkg/common/cliprinter/flag_test.go @@ -6,12 +6,14 @@ import ( "testing" agentapi "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" + commoncli "github.com/spiffe/spire/pkg/common/cli" ) func TestAppendFlag(t *testing.T) { flagCases := []struct { name string input []string + extraFlags []string expectedFormat formatType expectError bool }{ @@ -22,27 +24,40 @@ func TestAppendFlag(t *testing.T) { }, { name: "requires a value", - input: []string{"-format"}, + input: []string{"-output"}, expectError: true, }, + { + name: "error when setting a different value more than once", + input: []string{"-output", "json", "-format", "pretty"}, + extraFlags: []string{"format"}, + expectError: true, + }, + { + name: "works when setting the same value more than once", + input: []string{"-output", "pretty", "-format", "pretty"}, + extraFlags: []string{"format"}, + expectedFormat: pretty, + expectError: false, + }, { name: "requires a valid format", - input: []string{"-format", "nonexistent"}, + input: []string{"-output", "nonexistent"}, expectError: true, }, { name: "works when specifying pretty print", - input: []string{"-format", "pretty"}, + input: []string{"-output", "pretty"}, expectedFormat: pretty, }, { name: "works when specifying json", - input: []string{"-format", "json"}, + input: []string{"-output", "json"}, expectedFormat: json, }, { name: "input is case insensitive", - input: []string{"-format", "jSoN"}, + input: []string{"-output", "jSoN"}, expectedFormat: json, }, } @@ -53,8 +68,10 @@ func TestAppendFlag(t *testing.T) { fs := flag.NewFlagSet("testy", flag.ContinueOnError) fs.SetOutput(new(bytes.Buffer)) - AppendFlag(&p, fs) - + defaultFlagValue := AppendFlag(&p, fs, nil) + for _, flagName := range c.extraFlags { + fs.Var(defaultFlagValue, flagName, "") + } err := fs.Parse(c.input) switch { case err == nil: @@ -87,7 +104,7 @@ func TestAppendFlagWithCustomPretty(t *testing.T) { var p Printer fs := flag.NewFlagSet("testy", flag.ContinueOnError) - AppendFlagWithCustomPretty(&p, fs, nil) + AppendFlagWithCustomPretty(&p, fs, nil, nil) err := fs.Parse([]string{""}) if err != nil { t.Fatalf("error when configured with nil pretty func: %v", err) @@ -96,12 +113,12 @@ func TestAppendFlagWithCustomPretty(t *testing.T) { p = nil fs = flag.NewFlagSet("testy", flag.ContinueOnError) invoked := make(chan struct{}, 1) - cp := func(_ ...interface{}) error { + cp := func(_ *commoncli.Env, _ ...interface{}) error { invoked <- struct{}{} return nil } - AppendFlagWithCustomPretty(&p, fs, cp) - err = fs.Parse([]string{"-format", "pretty"}) + AppendFlagWithCustomPretty(&p, fs, nil, cp) + err = fs.Parse([]string{"-output", "pretty"}) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pkg/common/cliprinter/internal/protojson/protojson.go b/pkg/common/cliprinter/internal/protojson/protojson.go index c115264182..ec40d1fdb1 100644 --- a/pkg/common/cliprinter/internal/protojson/protojson.go +++ b/pkg/common/cliprinter/internal/protojson/protojson.go @@ -17,7 +17,8 @@ func Print(msgs []proto.Message, stdout, stderr io.Writer) error { jms := []json.RawMessage{} m := &protojson.MarshalOptions{ - UseProtoNames: true, + UseProtoNames: true, + EmitUnpopulated: true, } // Unfortunately, we can only marshal one message at a time, so diff --git a/pkg/common/cliprinter/internal/protojson/protojson_test.go b/pkg/common/cliprinter/internal/protojson/protojson_test.go index fe86239c82..3ae736bd7b 100644 --- a/pkg/common/cliprinter/internal/protojson/protojson_test.go +++ b/pkg/common/cliprinter/internal/protojson/protojson_test.go @@ -40,6 +40,12 @@ func TestPrint(t *testing.T) { stdout: "", stderr: "", }, + { + name: "message_with_unpopulated_fields", + protoFunc: unpopulatedFieldsMessage, + stdout: "{\"count\":0}\n", + stderr: "", + }, } for _, c := range cases { @@ -63,6 +69,14 @@ func normalCountAgentsResponseMessage(_ *testing.T) []proto.Message { } } +func unpopulatedFieldsMessage(_ *testing.T) []proto.Message { + return []proto.Message{ + &agentapi.CountAgentsResponse{ + Count: int32(0), + }, + } +} + func doubleCountAgentsResponseMessage(t *testing.T) []proto.Message { return []proto.Message{ normalCountAgentsResponseMessage(t)[0], diff --git a/pkg/common/diskutil/file_posix.go b/pkg/common/diskutil/file_posix.go index ddee077ba8..09b56d3a82 100644 --- a/pkg/common/diskutil/file_posix.go +++ b/pkg/common/diskutil/file_posix.go @@ -8,16 +8,53 @@ import ( "path/filepath" ) -// AtomicWriteFile writes data out. It writes to a temp file first, fsyncs that file, -// then swaps the file in. os.Rename is an atomic operation, so this sequence avoids having -// a partially written file at the final location. Finally, fsync is called on the directory -// to ensure the rename is persisted. -func AtomicWriteFile(path string, data []byte, mode os.FileMode) error { +const ( + fileModePrivate = 0600 + fileModePubliclyReadable = 0644 +) + +// AtomicWritePrivateFile writes data out to a private file. +// It writes to a temp file first, fsyncs that file, then swaps the file in. +// It renames the file using MoveFileEx with 'MOVEFILE_WRITE_THROUGH', +// which waits until the file is synced to disk. +func AtomicWritePrivateFile(path string, data []byte) error { + return atomicWrite(path, data, fileModePrivate) +} + +// AtomicWritePubliclyReadableFile writes data out to a publicly readable file. +// It writes to a temp file first, fsyncs that file, then swaps the file in. +// It renames the file using MoveFileEx with 'MOVEFILE_WRITE_THROUGH', +// which waits until the file is synced to disk. +func AtomicWritePubliclyReadableFile(path string, data []byte) error { + return atomicWrite(path, data, fileModePubliclyReadable) +} + +func CreateDataDirectory(path string) error { + return os.MkdirAll(path, 0755) +} + +// WritePrivateFile writes data out to a private file. The file is created if it +// does not exist. If exists, it's overwritten. +func WritePrivateFile(path string, data []byte) error { + return write(path, data, fileModePrivate, false) +} + +// WritePubliclyReadableFile writes data out to a publicly readable file. The +// file is created if it does not exist. If exists, it's overwritten. +func WritePubliclyReadableFile(path string, data []byte) error { + return write(path, data, fileModePubliclyReadable, false) +} + +func atomicWrite(path string, data []byte, mode os.FileMode) error { tmpPath := path + ".tmp" - if err := write(tmpPath, data, mode); err != nil { + if err := write(tmpPath, data, mode, true); err != nil { return err } + return rename(tmpPath, path) +} + +func rename(tmpPath, path string) error { if err := os.Rename(tmpPath, path); err != nil { return err } @@ -35,11 +72,11 @@ func AtomicWriteFile(path string, data []byte, mode os.FileMode) error { return dir.Close() } -func CreateDataDirectory(path string) error { - return os.MkdirAll(path, 0755) -} - -func write(tmpPath string, data []byte, mode os.FileMode) error { +// write writes to a file in the specified path with the specified +// security descriptor using the provided data. The sync boolean +// argument is used to indicate whether flushing to disk is required +// or not. +func write(tmpPath string, data []byte, mode os.FileMode, sync bool) error { file, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) if err != nil { return err @@ -50,9 +87,11 @@ func write(tmpPath string, data []byte, mode os.FileMode) error { return err } - if err := file.Sync(); err != nil { - file.Close() - return err + if sync { + if err := file.Sync(); err != nil { + file.Close() + return err + } } return file.Close() diff --git a/pkg/common/diskutil/file_posix_test.go b/pkg/common/diskutil/file_posix_test.go new file mode 100644 index 0000000000..02a629452d --- /dev/null +++ b/pkg/common/diskutil/file_posix_test.go @@ -0,0 +1,115 @@ +//go:build !windows +// +build !windows + +package diskutil + +import ( + "os" + "path/filepath" + "testing" + + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/require" +) + +func TestWriteFile(t *testing.T) { + dir := spiretest.TempDir(t) + + tests := []struct { + name string + data []byte + atomicWriteFunc func(string, []byte) error + expectMode os.FileMode + }{ + { + name: "basic - AtomicWritePrivateFile", + data: []byte("Hello, World"), + atomicWriteFunc: AtomicWritePrivateFile, + expectMode: 0600, + }, + { + name: "empty - AtomicWritePrivateFile", + data: []byte{}, + atomicWriteFunc: AtomicWritePrivateFile, + expectMode: 0600, + }, + { + name: "binary - AtomicWritePrivateFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + atomicWriteFunc: AtomicWritePrivateFile, + expectMode: 0600, + }, + { + name: "basic - AtomicWritePubliclyReadableFile", + data: []byte("Hello, World"), + atomicWriteFunc: AtomicWritePubliclyReadableFile, + expectMode: 0644, + }, + { + name: "empty - AtomicWritePubliclyReadableFile", + data: []byte{}, + atomicWriteFunc: AtomicWritePubliclyReadableFile, + expectMode: 0644, + }, + { + name: "binary - AtomicWritePubliclyReadableFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + atomicWriteFunc: AtomicWritePubliclyReadableFile, + expectMode: 0644, + }, + { + name: "basic - WritePrivateFile", + data: []byte("Hello, World"), + atomicWriteFunc: WritePrivateFile, + expectMode: 0600, + }, + { + name: "empty - WritePrivateFile", + data: []byte{}, + atomicWriteFunc: WritePrivateFile, + expectMode: 0600, + }, + { + name: "binary - WritePrivateFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + atomicWriteFunc: WritePrivateFile, + expectMode: 0600, + }, + { + name: "basic - WritePubliclyReadableFile", + data: []byte("Hello, World"), + atomicWriteFunc: WritePubliclyReadableFile, + expectMode: 0644, + }, + { + name: "empty - WritePubliclyReadableFile", + data: []byte{}, + atomicWriteFunc: WritePubliclyReadableFile, + expectMode: 0644, + }, + { + name: "binary - WritePubliclyReadableFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + atomicWriteFunc: WritePubliclyReadableFile, + expectMode: 0644, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + file := filepath.Join(dir, "file") + err := tt.atomicWriteFunc(file, tt.data) + require.NoError(t, err) + + info, err := os.Stat(file) + require.NoError(t, err) + require.EqualValues(t, tt.expectMode, info.Mode()) + + content, err := os.ReadFile(file) + require.NoError(t, err) + require.Equal(t, tt.data, content) + + require.NoError(t, os.Remove(file)) + }) + } +} diff --git a/pkg/common/diskutil/file_test.go b/pkg/common/diskutil/file_test.go deleted file mode 100644 index 6389af3f36..0000000000 --- a/pkg/common/diskutil/file_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package diskutil - -import ( - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/spiffe/spire/test/spiretest" - "github.com/stretchr/testify/require" -) - -func TestAtomicWriteFile(t *testing.T) { - dir := spiretest.TempDir(t) - - tests := []struct { - name string - data []byte - mode os.FileMode - expectPosixMode os.FileMode - expectWindowsMode os.FileMode - }{ - { - name: "basic test", - data: []byte("Hello, World"), - mode: 0600, - expectPosixMode: 0600, - expectWindowsMode: 0666, - }, - { - name: "empty", - data: []byte{}, - mode: 0440, - expectPosixMode: 0440, - expectWindowsMode: 0444, - }, - { - name: "binary", - data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, - mode: 0644, - expectPosixMode: 0644, - expectWindowsMode: 0666, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - file := filepath.Join(dir, "file") - err := AtomicWriteFile(file, tt.data, tt.mode) - require.NoError(t, err) - - info, err := os.Stat(file) - require.NoError(t, err) - switch runtime.GOOS { - case "windows": - require.EqualValues(t, tt.expectWindowsMode, info.Mode()) - default: - require.EqualValues(t, tt.expectPosixMode, info.Mode()) - } - - content, err := os.ReadFile(file) - require.NoError(t, err) - require.Equal(t, tt.data, content) - - require.NoError(t, os.Remove(file)) - }) - } -} diff --git a/pkg/common/diskutil/file_windows.go b/pkg/common/diskutil/file_windows.go index d440d32719..b315d2ce9d 100644 --- a/pkg/common/diskutil/file_windows.go +++ b/pkg/common/diskutil/file_windows.go @@ -4,6 +4,7 @@ package diskutil import ( + "fmt" "os" "syscall" "unsafe" @@ -17,16 +18,25 @@ const ( movefileWriteThrough = 0x8 ) -// AtomicWriteFile writes data out. It writes to a temp file first, fsyncs that file, -// then swaps the file in. Rename file using a custom MoveFileEx that uses 'MOVEFILE_WRITE_THROUGH' witch waits until -// file is synced to disk. -func AtomicWriteFile(path string, data []byte, mode os.FileMode) error { - tmpPath := path + ".tmp" - if err := write(tmpPath, data, mode); err != nil { - return err - } +type fileAttribs struct { + pathUTF16Ptr *uint16 + sa *windows.SecurityAttributes +} - return atomicRename(tmpPath, path) +// AtomicWritePrivateFile writes data out to a private file. +// It writes to a temp file first, fsyncs that file, then swaps the file in. +// It renames the file using MoveFileEx with 'MOVEFILE_WRITE_THROUGH', +// which waits until the file is synced to disk. +func AtomicWritePrivateFile(path string, data []byte) error { + return atomicWrite(path, data, sddl.PrivateFile) +} + +// AtomicWritePubliclyReadableFile writes data out to a publicly readable file. +// It writes to a temp file first, fsyncs that file, then swaps the file in. +// It renames the file using MoveFileEx with 'MOVEFILE_WRITE_THROUGH', +// which waits until the file is synced to disk. +func AtomicWritePubliclyReadableFile(path string, data []byte) error { + return atomicWrite(path, data, sddl.PubliclyReadableFile) } func CreateDataDirectory(path string) error { @@ -78,25 +88,75 @@ func MkdirAll(path string, sddl string) error { return nil } -func write(tmpPath string, data []byte, mode os.FileMode) error { - file, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) +// WritePrivateFile writes data out to a private file. The file is created if it +// does not exist. If exists, it's overwritten. +func WritePrivateFile(path string, data []byte) error { + return write(path, data, sddl.PrivateFile, false) +} + +// WritePubliclyReadableFile writes data out to a publicly readable file. The +// file is created if it does not exist. If exists, it's overwritten. +func WritePubliclyReadableFile(path string, data []byte) error { + return write(path, data, sddl.PubliclyReadableFile, false) +} + +func atomicWrite(path string, data []byte, sddl string) error { + tmpPath := path + ".tmp" + if err := write(tmpPath, data, sddl, true); err != nil { + return err + } + + return atomicRename(tmpPath, path) +} + +// write writes to a file in the specified path with the specified +// security descriptor using the provided data. The sync boolean +// argument is used to indicate whether flushing to disk is required +// or not. +func write(path string, data []byte, sddl string, sync bool) error { + handle, err := createFileForWriting(path, sddl) if err != nil { return err } + file := os.NewFile(uintptr(handle), path) + if file == nil { + return fmt.Errorf("invalid file descriptor for file %q", path) + } if _, err := file.Write(data); err != nil { file.Close() - return err + return fmt.Errorf("failed to write to file %q: %w", path, err) } - if err := file.Sync(); err != nil { - file.Close() - return err + if sync { + if err := file.Sync(); err != nil { + file.Close() + return fmt.Errorf("failed to sync file %q: %w", path, err) + } } return file.Close() } +func createFileForWriting(path string, sddl string) (windows.Handle, error) { + file, err := getFileWithSecurityAttr(path, sddl) + if err != nil { + return windows.InvalidHandle, err + } + handle, err := windows.CreateFile(file.pathUTF16Ptr, + windows.GENERIC_WRITE, + 0, + file.sa, + windows.CREATE_ALWAYS, + windows.FILE_ATTRIBUTE_NORMAL, + 0) + + if err != nil { + return windows.InvalidHandle, fmt.Errorf("could not create file %q: %w", path, err) + } + return handle, nil +} + func atomicRename(oldPath, newPath string) error { if err := rename(oldPath, newPath); err != nil { return &os.LinkError{ @@ -129,23 +189,35 @@ func rename(oldPath, newPath string) error { // // In the same way as os.MkDir, errors returned are of type *os.PathError. func mkdir(path string, sddl string) error { - sa := windows.SecurityAttributes{Length: 0} - sd, err := windows.SecurityDescriptorFromString(sddl) + file, err := getFileWithSecurityAttr(path, sddl) if err != nil { - return &os.PathError{Op: "mkdir", Path: path, Err: err} + return err } - sa.Length = uint32(unsafe.Sizeof(sa)) - sa.InheritHandle = 1 - sa.SecurityDescriptor = sd - pathUTF16, err := windows.UTF16PtrFromString(path) + err = windows.CreateDirectory(file.pathUTF16Ptr, file.sa) + if err != nil { + return fmt.Errorf("could not create directory: %w", err) + } + return nil +} + +func getFileWithSecurityAttr(path, sddl string) (*fileAttribs, error) { + sd, err := windows.SecurityDescriptorFromString(sddl) if err != nil { - return &os.PathError{Op: "mkdir", Path: path, Err: err} + return nil, fmt.Errorf("could not convert SDDL %q into a self-relative security descriptor object: %w", sddl, err) } - e := windows.CreateDirectory(pathUTF16, &sa) - if e != nil { - return &os.PathError{Op: "mkdir", Path: path, Err: e} + pathUTF16Ptr, err := windows.UTF16PtrFromString(path) + if err != nil { + return nil, fmt.Errorf("could not get pointer to the UTF-16 encoding of path %q: %w", path, err) } - return nil + + return &fileAttribs{ + pathUTF16Ptr: pathUTF16Ptr, + sa: &windows.SecurityAttributes{ + InheritHandle: 1, + Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), + SecurityDescriptor: sd, + }, + }, nil } diff --git a/pkg/common/diskutil/file_windows_test.go b/pkg/common/diskutil/file_windows_test.go new file mode 100644 index 0000000000..99c7c6fb7a --- /dev/null +++ b/pkg/common/diskutil/file_windows_test.go @@ -0,0 +1,131 @@ +//go:build windows +// +build windows + +package diskutil + +import ( + "os" + "path/filepath" + "testing" + + "github.com/spiffe/spire/pkg/common/sddl" + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func TestWriteFile(t *testing.T) { + dir := spiretest.TempDir(t) + + tests := []struct { + name string + data []byte + expectSecurityDescriptor string + atomicWriteFunc func(string, []byte) error + }{ + { + name: "basic - AtomicWritePrivateFile", + data: []byte("Hello, World"), + expectSecurityDescriptor: sddl.PrivateFile, + atomicWriteFunc: AtomicWritePrivateFile, + }, + { + name: "empty - AtomicWritePrivateFile", + data: []byte{}, + expectSecurityDescriptor: sddl.PrivateFile, + atomicWriteFunc: AtomicWritePrivateFile, + }, + { + name: "binary - AtomicWritePrivateFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + expectSecurityDescriptor: sddl.PrivateFile, + atomicWriteFunc: AtomicWritePrivateFile, + }, + { + name: "basic - AtomicWritePubliclyReadableFile", + data: []byte("Hello, World"), + expectSecurityDescriptor: sddl.PubliclyReadableFile, + atomicWriteFunc: AtomicWritePubliclyReadableFile, + }, + { + name: "empty - AtomicWritePubliclyReadableFile", + data: []byte{}, + expectSecurityDescriptor: sddl.PubliclyReadableFile, + atomicWriteFunc: AtomicWritePubliclyReadableFile, + }, + { + name: "binary - AtomicWritePubliclyReadableFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + expectSecurityDescriptor: sddl.PubliclyReadableFile, + atomicWriteFunc: AtomicWritePubliclyReadableFile, + }, + { + name: "basic - WritePrivateFile", + data: []byte("Hello, World"), + expectSecurityDescriptor: sddl.PrivateFile, + atomicWriteFunc: WritePrivateFile, + }, + { + name: "empty - WritePrivateFile", + data: []byte{}, + expectSecurityDescriptor: sddl.PrivateFile, + atomicWriteFunc: WritePrivateFile, + }, + { + name: "binary - WritePrivateFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + expectSecurityDescriptor: sddl.PrivateFile, + atomicWriteFunc: WritePrivateFile, + }, + { + name: "basic - WritePubliclyReadableFile", + data: []byte("Hello, World"), + expectSecurityDescriptor: sddl.PubliclyReadableFile, + atomicWriteFunc: WritePubliclyReadableFile, + }, + { + name: "empty - WritePubliclyReadableFile", + data: []byte{}, + expectSecurityDescriptor: sddl.PubliclyReadableFile, + atomicWriteFunc: WritePubliclyReadableFile, + }, + { + name: "binary - WritePubliclyReadableFile", + data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, + expectSecurityDescriptor: sddl.PubliclyReadableFile, + atomicWriteFunc: WritePubliclyReadableFile, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + file := filepath.Join(dir, "file") + err := tt.atomicWriteFunc(file, tt.data) + require.NoError(t, err) + + pathUTF16Ptr, err := windows.UTF16PtrFromString(file) + require.NoError(t, err) + + handle, err := windows.CreateFile(pathUTF16Ptr, + windows.GENERIC_WRITE, + 0, + nil, + windows.OPEN_EXISTING, + windows.FILE_ATTRIBUTE_NORMAL, + 0) + + require.NoError(t, err) + sd, err := windows.GetSecurityInfo(handle, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION) + require.NoError(t, windows.CloseHandle(handle)) + require.NoError(t, err) + + require.Equal(t, sd.String(), tt.expectSecurityDescriptor) + + content, err := os.ReadFile(file) + require.NoError(t, err) + require.Equal(t, tt.data, content) + + require.NoError(t, os.Remove(file)) + }) + } +} diff --git a/pkg/common/entrypoint/entrypoint_posix.go b/pkg/common/entrypoint/entrypoint_posix.go new file mode 100644 index 0000000000..cca596d704 --- /dev/null +++ b/pkg/common/entrypoint/entrypoint_posix.go @@ -0,0 +1,23 @@ +//go:build !windows +// +build !windows + +package entrypoint + +import ( + "context" + "os" +) + +type EntryPoint struct { + runCmdFn func(ctx context.Context, args []string) int +} + +func NewEntryPoint(runFn func(ctx context.Context, args []string) int) *EntryPoint { + return &EntryPoint{ + runCmdFn: runFn, + } +} + +func (e *EntryPoint) Main() int { + return e.runCmdFn(context.Background(), os.Args[1:]) +} diff --git a/pkg/common/entrypoint/entrypoint_posix_test.go b/pkg/common/entrypoint/entrypoint_posix_test.go new file mode 100644 index 0000000000..81be978781 --- /dev/null +++ b/pkg/common/entrypoint/entrypoint_posix_test.go @@ -0,0 +1,21 @@ +//go:build !windows +// +build !windows + +package entrypoint + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEntryPoint(t *testing.T) { + assert.Equal(t, + NewEntryPoint(func(ctx context.Context, args []string) int { return 0 }).Main(), + 0) + + assert.Equal(t, + NewEntryPoint(func(ctx context.Context, args []string) int { return 1 }).Main(), + 1) +} diff --git a/pkg/common/entrypoint/entrypoint_windows.go b/pkg/common/entrypoint/entrypoint_windows.go new file mode 100644 index 0000000000..a7694c1ce3 --- /dev/null +++ b/pkg/common/entrypoint/entrypoint_windows.go @@ -0,0 +1,73 @@ +//go:build windows +// +build windows + +package entrypoint + +import ( + "context" + "fmt" + "os" + + "golang.org/x/sys/windows/svc" +) + +type systemCaller interface { + IsWindowsService() (bool, error) + Run(name string, handler svc.Handler) error +} + +type systemCall struct { +} + +func (s *systemCall) IsWindowsService() (bool, error) { + return svc.IsWindowsService() +} + +func (s *systemCall) Run(name string, handler svc.Handler) error { + return svc.Run(name, handler) +} + +type EntryPoint struct { + handler svc.Handler + runCmdFn func(ctx context.Context, args []string) int + sc systemCaller +} + +func NewEntryPoint(runCmdFn func(ctx context.Context, args []string) int) *EntryPoint { + return &EntryPoint{ + runCmdFn: runCmdFn, + handler: &service{ + executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int { + defer stop() + retCode := runCmdFn(ctx, args[1:]) + return retCode + }, + }, + sc: &systemCall{}, + } +} + +func (e *EntryPoint) Main() int { + // Determining if SPIRE is running as a Windows service is done + // with a best-effort approach. If there is an error, just fallback + // to the behavior of not running as a Windows service. + isWindowsService, err := e.sc.IsWindowsService() + if err != nil { + fmt.Fprintf(os.Stderr, "Could not determine if running as a Windows service: %v", err) + } + if isWindowsService { + errChan := make(chan error) + go func() { + // Since the service runs in its own process, the service name is ignored. + // https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-startservicectrldispatcherw + errChan <- e.sc.Run("", e.handler) + }() + err = <-errChan + if err != nil { + return 1 + } + return 0 + } + + return e.runCmdFn(context.Background(), os.Args[1:]) +} diff --git a/pkg/common/entrypoint/entrypoint_windows_test.go b/pkg/common/entrypoint/entrypoint_windows_test.go new file mode 100644 index 0000000000..b3a69670b9 --- /dev/null +++ b/pkg/common/entrypoint/entrypoint_windows_test.go @@ -0,0 +1,323 @@ +//go:build windows +// +build windows + +package entrypoint + +import ( + "context" + "errors" + "sync" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" +) + +var runArgs = []string{"process-name", "run"} + +type fakeSystemCall struct { + mtx sync.RWMutex + args []string + exitCode uint32 + isWindowsService bool + isWindowsServiceErr error + runErr error + s service + svcSpecificEC bool + changeRequestCh chan svc.ChangeRequest + statusCh chan svc.Status +} + +func (s *fakeSystemCall) initChannels() { + s.mtx.Lock() + defer s.mtx.Unlock() + + s.changeRequestCh = make(chan svc.ChangeRequest, 1) + s.statusCh = make(chan svc.Status, 1) +} + +func (s *fakeSystemCall) IsWindowsService() (bool, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.isWindowsService, s.isWindowsServiceErr +} + +func (s *fakeSystemCall) Run(name string, handler svc.Handler) error { + var ( + wg sync.WaitGroup + svcSpecificEC bool + exitCode uint32 + ) + + wg.Add(1) + go func() { + defer wg.Done() + s.mtx.RLock() + defer s.mtx.RUnlock() + + svcSpecificEC, exitCode = s.s.Execute(s.args, s.changeRequestCh, s.statusCh) + }() + + c := make(chan struct{}) + go func() { + defer close(c) + wg.Wait() + }() + select { + case <-c: + case <-time.After(time.Minute): + panic("timed out") + } + + s.statusCh <- svc.Status{State: svc.Stopped} + + s.mtx.Lock() + defer s.mtx.Unlock() + s.svcSpecificEC = svcSpecificEC + s.exitCode = exitCode + return s.runErr +} + +func newEntryPoint(runCmdFn func(ctx context.Context, args []string) int, sc systemCaller) *EntryPoint { + return &EntryPoint{ + handler: &service{ + executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int { + retCode := runCmdFn(ctx, args[1:]) + defer stop() + return retCode + }, + }, + runCmdFn: runCmdFn, + sc: sc, + } +} + +func TestNotAService(t *testing.T) { + tests := []struct { + name string + retCode int + expectRunErr string + sc *fakeSystemCall + }{ + { + name: "success", + sc: &fakeSystemCall{}, + }, + { + name: "failure", + retCode: 1, + sc: &fakeSystemCall{}, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + retCodeCh := make(chan int, 1) + + go func() { + ep := newEntryPoint(func(ctx context.Context, args []string) int { + return testCase.retCode + }, testCase.sc) + retCodeCh <- ep.Main() + assert.True(t, true) + }() + + assertWithTimeout(t, testCase.retCode, retCodeCh) + }) + } +} + +func TestService(t *testing.T) { + tests := []struct { + name string + runCmdRetCode int + executeServiceFailure bool + expectRunErr string + sc *fakeSystemCall + }{ + { + name: "success", + sc: &fakeSystemCall{ + args: runArgs, + s: service{ + executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int { + return 0 + }, + }, + isWindowsService: true, + }, + }, + { + name: "fatal app exit", + executeServiceFailure: true, + sc: &fakeSystemCall{ + args: runArgs, + s: service{ + executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int { + stop() + return 1 + }, + }, + isWindowsService: true, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + retCodeCh := make(chan int, 1) + go func() { + ep := newEntryPoint(func(ctx context.Context, args []string) int { + return testCase.runCmdRetCode + }, testCase.sc) + retCodeCh <- ep.Main() + }() + + testCase.sc.initChannels() + + // This is running as a service. + // Check if we expect a failure running the service. + if testCase.executeServiceFailure { + // First status of the service should be Running. + waitForServiceState(t, testCase.sc.statusCh, svc.Running) + + // Since there was a failure, it should transition to Stopped, + // first having the StopPending status. + waitForServiceState(t, testCase.sc.statusCh, svc.StopPending) + + // Final status should be Stopped. + waitForServiceState(t, testCase.sc.statusCh, svc.Stopped) + + // Assert the return code for Main(). + assertWithTimeout(t, testCase.runCmdRetCode, retCodeCh) + + assert.False(t, testCase.sc.svcSpecificEC) + assert.Equal(t, uint32(windows.ERROR_FATAL_APP_EXIT), testCase.sc.exitCode) + return + } + + status := <-testCase.sc.statusCh + assert.Equal(t, svc.Running, status.State) + + // Interrogate the service, which should return the current status. + testCase.sc.changeRequestCh <- svc.ChangeRequest{ + Cmd: svc.Interrogate, + CurrentStatus: status, + } + + waitForServiceState(t, testCase.sc.statusCh, status.State) + + // Stop the service. Status should reflect that's pending to stop. + testCase.sc.changeRequestCh <- svc.ChangeRequest{Cmd: svc.Stop} + waitForServiceState(t, testCase.sc.statusCh, svc.StopPending) + + // Next status should be Stopped. + waitForServiceState(t, testCase.sc.statusCh, svc.Stopped) + }) + } +} + +func TestRunSvcFailure(t *testing.T) { + tests := []struct { + name string + runCmdRetCode int + expectRunErr string + sc *fakeSystemCall + }{ + { + name: "svc.Run failure", + runCmdRetCode: 1, + sc: &fakeSystemCall{ + args: runArgs, + runErr: errors.New("run error"), + s: service{ + executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int { + stop() + return 0 + }, + }, + isWindowsService: true, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + retCodeCh := make(chan int, 1) + go func() { + ep := newEntryPoint(func(ctx context.Context, args []string) int { + return testCase.runCmdRetCode + }, testCase.sc) + retCodeCh <- ep.Main() + }() + + testCase.sc.initChannels() + + // First status of the service should be Running. + waitForServiceState(t, testCase.sc.statusCh, svc.Running) + + // Since there was a failure, it should transition to Stopped, + // first having the StopPending status. + waitForServiceState(t, testCase.sc.statusCh, svc.StopPending) + + // Final status should be Stopped. + waitForServiceState(t, testCase.sc.statusCh, svc.Stopped) + + // Assert the return code for Main(). + assertWithTimeout(t, testCase.runCmdRetCode, retCodeCh) + }) + } +} + +func TestUnsupportedCommand(t *testing.T) { + tests := []struct { + name string + expectRetCode int + expectRunErr string + sc *fakeSystemCall + }{ + { + name: "service - unsupported command", + sc: &fakeSystemCall{ + args: []string{"bundle", "show"}, + s: service{ + executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int { + return 0 + }, + }, + isWindowsService: true, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + testCase.sc.initChannels() + + ep := newEntryPoint(func(ctx context.Context, args []string) int { + return 1 + }, testCase.sc) + assert.Equal(t, 0, ep.Main()) + assert.Equal(t, windows.ERROR_BAD_ARGUMENTS, syscall.Errno(testCase.sc.exitCode)) + }) + } +} + +func waitForServiceState(t *testing.T, statusCh chan svc.Status, state svc.State) { + select { + case status := <-statusCh: + assert.Equal(t, state, status.State) + case <-time.After(time.Second * 5): + require.FailNow(t, "timed out waiting for service state") + } +} + +func assertWithTimeout(t *testing.T, expectedRetCode int, retCodeCh chan int) { + select { + case <-time.After(time.Minute): + assert.FailNow(t, "timed out waiting for return code") + case retCode := <-retCodeCh: + assert.Equal(t, expectedRetCode, retCode) + } +} diff --git a/pkg/common/entrypoint/service_windows.go b/pkg/common/entrypoint/service_windows.go new file mode 100644 index 0000000000..b3b27ee2a5 --- /dev/null +++ b/pkg/common/entrypoint/service_windows.go @@ -0,0 +1,70 @@ +//go:build windows +// +build windows + +package entrypoint + +import ( + "context" + "sync" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" +) + +const supportedCommand = "run" + +type service struct { + mtx sync.RWMutex + executeServiceFn func(ctx context.Context, stop context.CancelFunc, args []string) int +} + +func (s *service) Execute(args []string, changeRequest <-chan svc.ChangeRequest, status chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { + // Validate that we are executing the "run" command. + // First argument (args[0]) is always the process name. Command name is + // expected in the second argument (args[1]). + if len(args) < 2 || args[1] != supportedCommand { + return false, uint32(windows.ERROR_BAD_ARGUMENTS) + } + + // Update the status to indicate that SPIRE is running. + // Only Stop and Shutdown commands are accepted (Interrogate is always accepted). + status <- svc.Status{ + State: svc.Running, + Accepts: svc.AcceptStop | svc.AcceptShutdown, + } + + var ( + wg sync.WaitGroup + retCode int + ) + ctx, stop := context.WithCancel(context.Background()) + wg.Add(1) + go func() { + defer wg.Done() + s.mtx.RLock() + defer s.mtx.RUnlock() + if retCode = s.executeServiceFn(ctx, stop, args); retCode != 0 { + retCode = int(windows.ERROR_FATAL_APP_EXIT) + } + }() + +loop: + for { + select { + case <-ctx.Done(): + break loop + case c := <-changeRequest: + switch c.Cmd { + case svc.Interrogate: + status <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + break loop + } + } + } + + status <- svc.Status{State: svc.StopPending} + stop() + wg.Wait() + return false, uint32(retCode) +} diff --git a/pkg/common/fflag/fflag.go b/pkg/common/fflag/fflag.go index 25d1d62b59..6420cbf7fc 100644 --- a/pkg/common/fflag/fflag.go +++ b/pkg/common/fflag/fflag.go @@ -56,8 +56,9 @@ var ( // Load initializes the fflag package and configures its feature flag state // based on the configuration input. Feature flags are designed to be -// Write-Once-Read-Many, and as such, Load can be called only once. Load will -// return an error if it is called more than once, if the configuration input +// Write-Once-Read-Many, and as such, Load can be called only once (except when Using Unload function +// for test scenarios, which will reset states enabling Load to be called again). +// Load will return an error if it is called more than once, if the configuration input // cannot be parsed, or if an unrecognized flag is set. func Load(rc RawConfig) error { singleton.mtx.Lock() @@ -91,6 +92,24 @@ func Load(rc RawConfig) error { return nil } +// Unload resets the feature flags states to its default values. This function is intended to be used for testing +// purposes only, it is not expected to be called by the normal execution of SPIRE. +func Unload() error { + singleton.mtx.Lock() + defer singleton.mtx.Unlock() + + if !singleton.loaded { + return errors.New("feature flags have not been loaded") + } + + for f := range singleton.flags { + singleton.flags[f] = false + } + + singleton.loaded = false + return nil +} + // IsSet can be used to determine whether or not a particular feature flag is // set. func IsSet(f Flag) bool { diff --git a/pkg/common/fflag/fflag_test.go b/pkg/common/fflag/fflag_test.go index f70161bc36..90d05f443a 100644 --- a/pkg/common/fflag/fflag_test.go +++ b/pkg/common/fflag/fflag_test.go @@ -2,6 +2,9 @@ package fflag import ( "testing" + + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/assert" ) func TestLoadOnce(t *testing.T) { @@ -94,6 +97,56 @@ func TestLoad(t *testing.T) { reset() } +func TestUnload(t *testing.T) { + type want struct { + errStr string + unloadedFlags []Flag + } + tests := []struct { + name string + setup func() + want want + }{ + { + name: "unload without loading", + setup: func() { + singleton.mtx.Lock() + defer singleton.mtx.Unlock() + singleton.loaded = false + }, + want: want{ + errStr: "feature flags have not been loaded", + }, + }, + { + name: "unload after loading", + setup: func() { + singleton.mtx.Lock() + defer singleton.mtx.Unlock() + singleton.loaded = true + singleton.flags[FlagTestFlag] = true + }, + want: want{ + unloadedFlags: []Flag{FlagTestFlag}, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + testCase.setup() + err := Unload() + if testCase.want.errStr == "" { + assert.NoError(t, err) + } else { + spiretest.AssertErrorContains(t, err, testCase.want.errStr) + } + for _, flag := range testCase.want.unloadedFlags { + assert.False(t, IsSet(flag)) + } + }) + } +} + func reset() { singleton.mtx.Lock() defer singleton.mtx.Unlock() diff --git a/pkg/common/health/cache.go b/pkg/common/health/cache.go new file mode 100644 index 0000000000..dcf5f1fd0b --- /dev/null +++ b/pkg/common/health/cache.go @@ -0,0 +1,211 @@ +package health + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/andres-erbsen/clock" + "github.com/sirupsen/logrus" + "github.com/spiffe/spire/pkg/common/telemetry" +) + +type checkState struct { + // err is the error returned from a failed health check + err error + + // details contains more contextual detail about a + // failing health check. + details State + + // checkTime is the time of the last health check + checkTime time.Time + + // contiguousFailures the number of failures that occurred in a row + contiguousFailures int64 + + // timeOfFirstFailure the time of the initial transitional failure for + // any given health check + timeOfFirstFailure time.Time +} + +type checkerSubsystem struct { + state checkState + checkable Checkable +} + +func newCache(log logrus.FieldLogger, clock clock.Clock) *cache { + return &cache{ + checkerSubsystems: make(map[string]*checkerSubsystem), + log: log, + clk: clock, + } +} + +type cache struct { + checkerSubsystems map[string]*checkerSubsystem + + mtx sync.RWMutex + clk clock.Clock + + log logrus.FieldLogger + hooks struct { + statusUpdated chan struct{} + } +} + +func (c *cache) addCheck(name string, checkable Checkable) error { + c.mtx.Lock() + defer c.mtx.Unlock() + + if _, ok := c.checkerSubsystems[name]; ok { + return fmt.Errorf("check %q has already been added", name) + } + + c.checkerSubsystems[name] = &checkerSubsystem{ + checkable: checkable, + } + return nil +} + +func (c *cache) getCheckerSubsystems() map[string]*checkerSubsystem { + c.mtx.RLock() + defer c.mtx.RUnlock() + + checkerSubsystems := make(map[string]*checkerSubsystem, len(c.checkerSubsystems)) + for k, v := range c.checkerSubsystems { + checkerSubsystems[k] = &checkerSubsystem{ + checkable: v.checkable, + state: v.state, + } + } + return checkerSubsystems +} + +func (c *cache) getStatuses() map[string]checkState { + c.mtx.RLock() + defer c.mtx.RUnlock() + + statuses := make(map[string]checkState, len(c.checkerSubsystems)) + for k, v := range c.checkerSubsystems { + statuses[k] = v.state + } + + return statuses +} + +func (c *cache) start(ctx context.Context) error { + c.mtx.RLock() + defer c.mtx.RUnlock() + + if len(c.checkerSubsystems) < 1 { + return errors.New("no health checks defined") + } + + c.startRunner(ctx) + return nil +} + +func (c *cache) startRunner(ctx context.Context) { + c.log.Debug("Initializing health checkers") + checkFunc := func() { + for name, checker := range c.getCheckerSubsystems() { + state, err := verifyStatus(checker.checkable) + + checkState := checkState{ + details: state, + checkTime: c.clk.Now(), + } + if err != nil { + c.log.WithField("check", name). + WithError(err). + Error("Health check has failed") + checkState.err = err + } + + c.setStatus(name, checker.state, checkState) + } + if c.hooks.statusUpdated != nil { + c.hooks.statusUpdated <- struct{}{} + } + } + + ticker := c.clk.Ticker(readyCheckInterval) + + go func() { + defer func() { + c.log.Debug("Finishing health checker") + ticker.Stop() + }() + for { + checkFunc() + + select { + case <-ticker.C: + case <-ctx.Done(): + return + } + } + }() +} + +func (c *cache) setStatus(name string, prevState checkState, state checkState) { + c.embellishState(name, &prevState, &state) + + c.mtx.Lock() + defer c.mtx.Unlock() + + // We are sure that checker exist in this plase, to be able to check + // status of a subsytem we must call the checker inside this map + c.checkerSubsystems[name].state = state +} + +func (c *cache) embellishState(name string, prevState, state *checkState) { + switch { + case state.err == nil && prevState.err == nil: + // All fine continue + case state.err != nil && prevState.err == nil: + // State start to fail, add log and set failures tracking + c.log.WithFields(logrus.Fields{ + telemetry.Check: name, + telemetry.Details: state.details, + telemetry.Error: state.err.Error(), + }).Warn("Health check failed") + + state.timeOfFirstFailure = c.clk.Now() + state.contiguousFailures = 1 + + case state.err != nil && prevState.err != nil: + // Error still happening, carry the time of first failure from the previous state + state.timeOfFirstFailure = prevState.timeOfFirstFailure + state.contiguousFailures = prevState.contiguousFailures + 1 + + case state.err == nil && prevState.err != nil: + // Current state has no error, notify about error recovering + failureSeconds := c.clk.Now().Sub(prevState.timeOfFirstFailure).Seconds() + c.log.WithFields(logrus.Fields{ + telemetry.Check: name, + telemetry.Details: state.details, + telemetry.Error: prevState.err.Error(), + telemetry.Failures: prevState.contiguousFailures, + telemetry.Duration: failureSeconds, + }).Info("Health check recovered") + } +} + +func verifyStatus(check Checkable) (State, error) { + state := check.CheckHealth() + var err error + switch { + case state.Ready && state.Live: + case state.Ready && !state.Live: + err = errors.New("subsystem is not live") + case !state.Ready && state.Live: + err = errors.New("subsystem is not ready") + case !state.Ready && !state.Live: + err = errors.New("subsystem is not live or ready") + } + return state, err +} diff --git a/pkg/common/health/cache_test.go b/pkg/common/health/cache_test.go new file mode 100644 index 0000000000..657824da9e --- /dev/null +++ b/pkg/common/health/cache_test.go @@ -0,0 +1,321 @@ +package health + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/andres-erbsen/clock" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/require" +) + +func TestAddCheck(t *testing.T) { + log, _ := test.NewNullLogger() + t.Run("add check no error", func(t *testing.T) { + c := newCache(log, clock.New()) + err := c.addCheck("foh", &fakeCheckable{}) + require.NoError(t, err) + }) + + t.Run("add duplicated checker", func(t *testing.T) { + c := newCache(log, clock.New()) + err := c.addCheck("foo", &fakeCheckable{}) + require.NoError(t, err) + + err = c.addCheck("bar", &fakeCheckable{}) + require.NoError(t, err) + + err = c.addCheck("foo", &fakeCheckable{}) + require.EqualError(t, err, `check "foo" has already been added`) + }) +} + +func TestStartNoCheckerSet(t *testing.T) { + clockMock := clock.NewMock() + + log, hook := test.NewNullLogger() + log.Level = logrus.DebugLevel + + c := newCache(log, clockMock) + + err := c.start(context.Background()) + require.EqualError(t, err, "no health checks defined") + require.Empty(t, hook.Entries) +} + +func TestHealthFailsAndRecover(t *testing.T) { + log, hook := test.NewNullLogger() + log.Level = logrus.DebugLevel + waitFor := make(chan struct{}, 1) + clockMock := clock.NewMock() + + c := newCache(log, clockMock) + c.hooks.statusUpdated = waitFor + + fooChecker := &fakeCheckable{ + state: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + } + barChecker := &fakeCheckable{ + state: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + } + + err := c.addCheck("foo", fooChecker) + require.NoError(t, err) + + err = c.addCheck("bar", barChecker) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + err = c.start(ctx) + require.NoError(t, err) + + t.Run("start successfully", func(t *testing.T) { + // Wait for initial calls + select { + case <-waitFor: + case <-ctx.Done(): + require.Fail(t, "unable to get updates because context is finished") + } + expectLogs := []spiretest.LogEntry{ + { + Level: logrus.DebugLevel, + Message: "Initializing health checkers", + }, + } + expectStatus := map[string]checkState{ + "foo": { + details: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + checkTime: clockMock.Now(), + }, + "bar": { + details: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + checkTime: clockMock.Now(), + }, + } + + spiretest.AssertLogs(t, hook.AllEntries(), expectLogs) + require.Equal(t, expectStatus, c.getStatuses()) + }) + + // Clean logs + hook.Reset() + + // Health start to fail + fooChecker.state = State{ + Live: false, + Ready: false, + LiveDetails: healthDetails{Err: "live is failing"}, + ReadyDetails: healthDetails{Err: "ready is failing"}, + } + + t.Run("health start to fail", func(t *testing.T) { + // Move to next interval + clockMock.Add(readyCheckInterval) + + // Wait for new call + select { + case <-waitFor: + case <-ctx.Done(): + require.Fail(t, "unable to get updates because context is finished") + } + + expectStatus := map[string]checkState{ + "foo": { + details: State{ + Live: false, + Ready: false, + LiveDetails: healthDetails{Err: "live is failing"}, + ReadyDetails: healthDetails{Err: "ready is failing"}, + }, + checkTime: clockMock.Now(), + err: errors.New("subsystem is not live or ready"), + contiguousFailures: 1, + timeOfFirstFailure: clockMock.Now(), + }, + "bar": { + details: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + checkTime: clockMock.Now(), + }, + } + + expectLogs := []spiretest.LogEntry{ + { + Level: logrus.ErrorLevel, + Message: "Health check has failed", + Data: logrus.Fields{ + telemetry.Check: "foo", + telemetry.Error: "subsystem is not live or ready", + }, + }, + { + Level: logrus.WarnLevel, + Message: "Health check failed", + Data: logrus.Fields{ + telemetry.Check: "foo", + telemetry.Details: "{false false {live is failing} {ready is failing}}", + telemetry.Error: "subsystem is not live or ready", + }, + }, + } + + spiretest.AssertLogs(t, hook.AllEntries(), expectLogs) + require.Equal(t, expectStatus, c.getStatuses()) + }) + + t.Run("health still failing", func(t *testing.T) { + hook.Reset() + previousFailureDate := clockMock.Now() + + // Move to next interval + clockMock.Add(readyCheckInterval) + + // Wait for new call + select { + case <-waitFor: + case <-ctx.Done(): + require.Fail(t, "unable to get updates because context is finished") + } + + expectStatus := map[string]checkState{ + "foo": { + details: State{ + Live: false, + Ready: false, + LiveDetails: healthDetails{Err: "live is failing"}, + ReadyDetails: healthDetails{Err: "ready is failing"}, + }, + checkTime: clockMock.Now(), + err: errors.New("subsystem is not live or ready"), + contiguousFailures: 2, + timeOfFirstFailure: previousFailureDate, + }, + "bar": { + details: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + checkTime: clockMock.Now(), + }, + } + + expectLogs := []spiretest.LogEntry{ + { + Level: logrus.ErrorLevel, + Message: "Health check has failed", + Data: logrus.Fields{ + telemetry.Check: "foo", + telemetry.Error: "subsystem is not live or ready", + }, + }, + } + + spiretest.AssertLogs(t, hook.AllEntries(), expectLogs) + require.Equal(t, expectStatus, c.getStatuses()) + }) + + // Health start to recover + fooChecker.state = State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + } + + t.Run("health recovered", func(t *testing.T) { + hook.Reset() + + // Move to next interval + clockMock.Add(readyCheckInterval) + + // Wait for new call + select { + case <-waitFor: + case <-ctx.Done(): + require.Fail(t, "unable to get updates because context is finished") + } + + expectStatus := map[string]checkState{ + "foo": { + details: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + checkTime: clockMock.Now(), + }, + "bar": { + details: State{ + Live: true, + Ready: true, + LiveDetails: healthDetails{}, + ReadyDetails: healthDetails{}, + }, + checkTime: clockMock.Now(), + }, + } + + expectLogs := []spiretest.LogEntry{ + { + Level: logrus.InfoLevel, + Message: "Health check recovered", + Data: logrus.Fields{ + telemetry.Check: "foo", + telemetry.Details: "{true true {} {}}", + telemetry.Duration: "120", + telemetry.Error: "subsystem is not live or ready", + telemetry.Failures: "2", + }, + }, + } + + spiretest.AssertLogs(t, hook.AllEntries(), expectLogs) + require.Equal(t, expectStatus, c.getStatuses()) + }) +} + +type fakeCheckable struct { + state State +} + +func (f *fakeCheckable) CheckHealth() State { + return f.state +} + +type healthDetails struct { + Err string `json:"err,omitempty"` +} diff --git a/pkg/common/health/health.go b/pkg/common/health/health.go index 8f6415e55d..40d9132617 100644 --- a/pkg/common/health/health.go +++ b/pkg/common/health/health.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/InVisionApp/go-health/v2" + "github.com/andres-erbsen/clock" "github.com/sirupsen/logrus" "github.com/spiffe/spire/pkg/common/telemetry" "google.golang.org/grpc" @@ -61,13 +61,14 @@ type ServableChecker interface { } func NewChecker(config Config, log logrus.FieldLogger) ServableChecker { - hc := health.New() - l := log.WithField(telemetry.SubsystemName, "health") - hc.StatusListener = &statusListener{log: l} - hc.Logger = &logadapter{FieldLogger: l} - c := &checker{config: config, hc: hc, log: l} + c := &checker{ + config: config, + log: l, + + cache: newCache(l, clock.New()), + } // Start HTTP server if ListenerEnabled is true if config.ListenerEnabled { @@ -91,29 +92,24 @@ type checker struct { server *http.Server - hc *health.Health - mutex sync.Mutex // Mutex protects non-threadsafe hc + mutex sync.Mutex // Mutex protects non-threadsafe - log logrus.FieldLogger + log logrus.FieldLogger + cache *cache } func (c *checker) AddCheck(name string, checkable Checkable) error { c.mutex.Lock() defer c.mutex.Unlock() - return c.hc.AddCheck(&health.Config{ - Name: name, - Checker: checkableWrapper{checkable: checkable}, - Interval: readyCheckInterval, - Fatal: true, - }) + return c.cache.addCheck(name, checkable) } func (c *checker) ListenAndServe(ctx context.Context) error { c.mutex.Lock() defer c.mutex.Unlock() - if err := c.hc.Start(); err != nil { + if err := c.cache.start(ctx); err != nil { return err } @@ -134,16 +130,12 @@ func (c *checker) ListenAndServe(ctx context.Context) error { defer wg.Done() <-ctx.Done() if c.server != nil { - c.server.Close() + _ = c.server.Close() } }() wg.Wait() - if err := c.hc.Stop(); err != nil { - c.log.WithError(err).Warn("Error stopping health checks") - } - return nil } @@ -169,32 +161,30 @@ func WaitForTestDial(ctx context.Context, addr net.Addr) { return } - conn.Close() + _ = conn.Close() } // LiveState returns the global live state and details. func (c *checker) LiveState() (bool, interface{}) { - states, _, _ := c.hc.State() - live, _, details, _ := c.checkStates(states) + live, _, details, _ := c.checkStates() return live, details } // ReadyState returns the global ready state and details. func (c *checker) ReadyState() (bool, interface{}) { - states, _, _ := c.hc.State() - _, ready, _, details := c.checkStates(states) + _, ready, _, details := c.checkStates() return ready, details } -func (c *checker) checkStates(states map[string]health.State) (bool, bool, interface{}, interface{}) { +func (c *checker) checkStates() (bool, bool, interface{}, interface{}) { isLive, isReady := true, true liveDetails := make(map[string]interface{}) readyDetails := make(map[string]interface{}) - for subsystemName, subsystemState := range states { - state := subsystemState.Details.(State) + for subsystemName, subsystemState := range c.cache.getStatuses() { + state := subsystemState.details if !state.Live { isLive = false } @@ -235,23 +225,3 @@ func (c *checker) readyHandler(w http.ResponseWriter, req *http.Request) { w.WriteHeader(statusCode) _ = json.NewEncoder(w).Encode(details) } - -// checkableWrapper wraps Checkable in something that conforms to health.ICheckable -type checkableWrapper struct { - checkable Checkable -} - -func (c checkableWrapper) Status() (interface{}, error) { - state := c.checkable.CheckHealth() - var err error - switch { - case state.Ready && state.Live: - case state.Ready && !state.Live: - err = errors.New("subsystem is not live") - case !state.Ready && state.Live: - err = errors.New("subsystem is not ready") - case !state.Ready && !state.Live: - err = errors.New("subsystem is not live or ready") - } - return state, err -} diff --git a/pkg/common/health/health_test.go b/pkg/common/health/health_test.go index 3cb626bcb6..1d96bb94f9 100644 --- a/pkg/common/health/health_test.go +++ b/pkg/common/health/health_test.go @@ -1,10 +1,18 @@ package health import ( + "context" + "io" + "net" + "net/http" "testing" + "time" + "github.com/andres-erbsen/clock" + "github.com/sirupsen/logrus/hooks/test" logtest "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestServerDisabledByDefault(t *testing.T) { @@ -20,3 +28,119 @@ func TestServerEnabled(t *testing.T) { assert.NotNil(t, checker.server) } + +func TestCheckerListeners(t *testing.T) { + log, _ := test.NewNullLogger() + config := Config{ + ListenerEnabled: true, + BindAddress: "localhost", + BindPort: "12345", + } + + servableChecker := NewChecker(config, log) + + fooCheker := &fakeCheckable{ + state: State{ + Live: true, + Ready: true, + ReadyDetails: healthDetails{}, + LiveDetails: healthDetails{}, + }, + } + err := servableChecker.AddCheck("foo", fooCheker) + require.NoError(t, err) + + barChecker := &fakeCheckable{ + state: State{ + Live: true, + Ready: true, + ReadyDetails: healthDetails{}, + LiveDetails: healthDetails{}, + }, + } + err = servableChecker.AddCheck("bar", barChecker) + require.NoError(t, err) + + // Get checker to set a chan in order to wait until sync is done + finalChecker, ok := servableChecker.(*checker) + require.True(t, ok) + + clk := clock.NewMock() + finalChecker.cache.clk = clk + + waitFor := make(chan struct{}, 1) + finalChecker.cache.hooks.statusUpdated = waitFor + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + go func() { + _ = servableChecker.ListenAndServe(ctx) + }() + + require.Eventuallyf(t, func() bool { + _, err := net.Dial("tcp", "localhost:12345") + return err == nil + }, time.Minute, 50*time.Millisecond, "server didn't started in the required time") + + t.Run("success ready", func(t *testing.T) { + resp, err := http.Get("http://localhost:12345/ready") + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + actual, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.JSONEq(t, "{\"bar\":{},\"foo\":{}}\n", string(actual)) + }) + + t.Run("success live", func(t *testing.T) { + resp, err := http.Get("http://localhost:12345/live") + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + actual, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.JSONEq(t, "{\"bar\":{},\"foo\":{}}\n", string(actual)) + }) + + fooCheker.state.Live = false + fooCheker.state.LiveDetails = healthDetails{Err: "live fails"} + + barChecker.state.Ready = false + barChecker.state.ReadyDetails = healthDetails{Err: "ready fails"} + + clk.Add(readyCheckInterval) + select { + case <-waitFor: + case <-ctx.Done(): + require.Fail(t, "unable to get updates") + } + + t.Run("live fails", func(t *testing.T) { + resp, err := http.Get("http://localhost:12345/live") + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + + actual, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.JSONEq(t, "{\"bar\":{},\"foo\":{\"err\":\"live fails\"}}\n", string(actual)) + }) + + t.Run("ready fails", func(t *testing.T) { + resp, err := http.Get("http://localhost:12345/ready") + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + + actual, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.JSONEq(t, "{\"bar\":{\"err\":\"ready fails\"},\"foo\":{}}\n", string(actual)) + }) +} diff --git a/pkg/common/health/logger.go b/pkg/common/health/logger.go deleted file mode 100644 index 66c41962d5..0000000000 --- a/pkg/common/health/logger.go +++ /dev/null @@ -1,45 +0,0 @@ -package health - -import ( - "github.com/InVisionApp/go-health/v2" - log "github.com/InVisionApp/go-logger" - "github.com/sirupsen/logrus" -) - -// statusListener logs -type statusListener struct { - log logrus.FieldLogger -} - -// Assert statusListener implements IStatusListener -var _ health.IStatusListener = &statusListener{} - -// HealthCheckFailed is triggered when a health check fails the first time -func (sl *statusListener) HealthCheckFailed(entry *health.State) { - sl.log.WithField("check", entry.Name). - WithField("details", entry.Details). - WithField("error", entry.Err). - Warn("Health check failed") -} - -// HealthCheckRecovered is triggered when a health check recovers -func (sl *statusListener) HealthCheckRecovered(entry *health.State, recordedFailures int64, failureDurationSeconds float64) { - sl.log.WithField("check", entry.Name). - WithField("details", entry.Details). - WithField("error", entry.Err). - WithField("failures", recordedFailures). - WithField("duration", failureDurationSeconds). - Info("Health check recovered") -} - -// logadapter adapts types between InVisionApp/go-logger and logrus -type logadapter struct { - logrus.FieldLogger -} - -// WithFields wraps logrus.Fieldlogger to implement the Logger interface in InVisionApp/go-logger -func (l *logadapter) WithFields(fields log.Fields) log.Logger { - return &logadapter{ - FieldLogger: l.FieldLogger.WithFields(logrus.Fields(fields)), - } -} diff --git a/pkg/common/log/hclog_adapter.go b/pkg/common/log/hclog_adapter.go index 25e908452d..498efade79 100644 --- a/pkg/common/log/hclog_adapter.go +++ b/pkg/common/log/hclog_adapter.go @@ -87,6 +87,12 @@ func (a *HCLogAdapter) SetLevel(hclog.Level) { // we don't currently. } +func (a *HCLogAdapter) GetLevel() hclog.Level { + // We don't support dynamically setting the level with SetLevel(), + // so just return a default value here. + return hclog.NoLevel +} + func (a *HCLogAdapter) With(args ...interface{}) hclog.Logger { e := a.CreateEntry(args) return &HCLogAdapter{ diff --git a/pkg/common/pemutil/block.go b/pkg/common/pemutil/block.go index 4345e72b61..f9e56a75fc 100644 --- a/pkg/common/pemutil/block.go +++ b/pkg/common/pemutil/block.go @@ -19,11 +19,11 @@ type Block struct { } func LoadBlocks(path string) ([]Block, error) { - return loadBlocks(path, 0, "") + return loadBlocks(path, 0) } func ParseBlocks(pemBytes []byte) ([]Block, error) { - return parseBlocks(pemBytes, 0, "") + return parseBlocks(pemBytes, 0) } func loadBlock(path string, expectedTypes ...string) (*Block, error) { diff --git a/pkg/common/pemutil/certs.go b/pkg/common/pemutil/certs.go index 46c5ac4455..0ef331e265 100644 --- a/pkg/common/pemutil/certs.go +++ b/pkg/common/pemutil/certs.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "encoding/pem" "fmt" - "os" ) func ParseCertificate(pemBytes []byte) (*x509.Certificate, error) { @@ -48,20 +47,12 @@ func EncodeCertificates(certs []*x509.Certificate) []byte { return buf.Bytes() } -func SaveCertificates(path string, certs []*x509.Certificate, mode os.FileMode) error { - return os.WriteFile(path, EncodeCertificates(certs), mode) -} - func EncodeCertificate(cert *x509.Certificate) []byte { var buf bytes.Buffer encodeCertificate(&buf, cert) return buf.Bytes() } -func SaveCertificate(path string, cert *x509.Certificate, mode os.FileMode) error { - return os.WriteFile(path, EncodeCertificate(cert), mode) -} - func certFromObject(object interface{}) (*x509.Certificate, error) { cert, ok := object.(*x509.Certificate) if !ok { diff --git a/pkg/common/pemutil/pemutil_test.go b/pkg/common/pemutil/pemutil_test.go index 981202dacf..03430f135a 100644 --- a/pkg/common/pemutil/pemutil_test.go +++ b/pkg/common/pemutil/pemutil_test.go @@ -4,7 +4,6 @@ import ( "crypto/ecdsa" "crypto/rsa" "os" - "path" "testing" "github.com/stretchr/testify/suite" @@ -272,38 +271,6 @@ func (s *Suite) TestEncodeCertificate() { s.Require().Equal(expCertPem, EncodeCertificate(cert)) } -func (s *Suite) TestSaveCertificate() { - dir, err := os.MkdirTemp("", "pemutil-test") - s.Require().NoError(err) - defer os.Remove(dir) - - certPath := path.Join(dir, "cert") - cert, err := LoadCertificate("testdata/cert.pem") - s.Require().NoError(err) - err = SaveCertificate(certPath, cert, 0600) - s.Require().NoError(err) - - fileContent, err := os.ReadFile(certPath) - s.Require().NoError(err) - s.Require().Equal(EncodeCertificate(cert), fileContent) -} - -func (s *Suite) TestSaveCertificates() { - dir, err := os.MkdirTemp("", "pemutil-test") - s.Require().NoError(err) - defer os.Remove(dir) - - certsPath := path.Join(dir, "certs") - certs, err := LoadCertificates("testdata/certs.pem") - s.Require().NoError(err) - err = SaveCertificates(certsPath, certs, 0600) - s.Require().NoError(err) - - fileContent, err := os.ReadFile(certsPath) - s.Require().NoError(err) - s.Require().Equal(EncodeCertificates(certs), fileContent) -} - func (s *Suite) TestLoadSigner() { // fail if not a private key _, err := LoadSigner("testdata/cert.pem") diff --git a/pkg/common/plugin/azure/msi.go b/pkg/common/plugin/azure/msi.go index 4868b144bb..201d36815e 100644 --- a/pkg/common/plugin/azure/msi.go +++ b/pkg/common/plugin/azure/msi.go @@ -5,9 +5,10 @@ import ( "encoding/json" "io" "net/http" - "net/url" - "path" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/spire/pkg/common/agentpathtemplate" + "github.com/spiffe/spire/pkg/common/idutil" "github.com/zeebo/errs" "gopkg.in/square/go-jose.v2/jwt" ) @@ -17,8 +18,12 @@ const ( // audience of the MSI token. The current value is the service ID for the // Resource Manager API. DefaultMSIResourceID = "https://management.azure.com/" + PluginName = "azure_msi" ) +// DefaultAgentPathTemplate is the default text/template +var DefaultAgentPathTemplate = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}") + type ComputeMetadata struct { Name string `json:"name"` SubscriptionID string `json:"subscriptionId"` @@ -35,16 +40,8 @@ type MSIAttestationData struct { type MSITokenClaims struct { jwt.Claims - TenantID string `json:"tid,omitempty"` -} - -func (c *MSITokenClaims) AgentID(trustDomain string) string { - u := url.URL{ - Scheme: "spiffe", - Host: trustDomain, - Path: path.Join("spire", "agent", "azure_msi", c.TenantID, c.Subject), - } - return u.String() + TenantID string `json:"tid,omitempty"` + PrincipalID string `json:"sub,omitempty"` } type HTTPClient interface { @@ -125,6 +122,23 @@ func FetchInstanceMetadata(ctx context.Context, cl HTTPClient) (*InstanceMetadat return metadata, nil } +type agentPathTemplateData struct { + MSITokenClaims + PluginName string +} + +func MakeAgentID(td spiffeid.TrustDomain, agentPathTemplate *agentpathtemplate.Template, claims *MSITokenClaims) (spiffeid.ID, error) { + agentPath, err := agentPathTemplate.Execute(agentPathTemplateData{ + MSITokenClaims: *claims, + PluginName: PluginName, + }) + if err != nil { + return spiffeid.ID{}, err + } + + return idutil.AgentID(td, agentPath) +} + func tryRead(r io.Reader) string { b := make([]byte, 1024) n, _ := r.Read(b) diff --git a/pkg/common/plugin/azure/msi_test.go b/pkg/common/plugin/azure/msi_test.go index f134e94b74..c8af4d87e2 100644 --- a/pkg/common/plugin/azure/msi_test.go +++ b/pkg/common/plugin/azure/msi_test.go @@ -2,26 +2,20 @@ package azure import ( "context" + "errors" "fmt" "io" "net/http" "strings" "testing" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/spire/pkg/common/agentpathtemplate" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2/jwt" ) -func TestMSITokenClaims(t *testing.T) { - claims := MSITokenClaims{ - Claims: jwt.Claims{ - Subject: "PRINCIPALID", - }, - TenantID: "TENANTID", - } - require.Equal(t, "spiffe://example.org/spire/agent/azure_msi/TENANTID/PRINCIPALID", claims.AgentID("example.org")) -} - func TestFetchMSIToken(t *testing.T) { ctx := context.Background() @@ -114,6 +108,75 @@ func TestFetchInstanceMetadata(t *testing.T) { require.Equal(t, expected, metadata) } +func TestMakeAgentID(t *testing.T) { + type args struct { + td string + agentPathTemplate string + claims *MSITokenClaims + } + tests := []struct { + name string + args args + want string + errWanted error + }{ + { + name: "successfully applies template", + args: args{ + td: "example.org", + agentPathTemplate: "/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}", + claims: &MSITokenClaims{ + Claims: jwt.Claims{}, + TenantID: "TENANTID", + PrincipalID: "PRINCIPALID", + }, + }, + want: "spiffe://example.org/spire/agent/azure_msi/TENANTID/PRINCIPALID", + errWanted: nil, + }, + { + name: "error applying template with non-existent field", + args: args{ + td: "example.org", + agentPathTemplate: "/{{ .PluginName }}/{{ .TenantID }}/{{ .NonExistent }}", + claims: &MSITokenClaims{ + Claims: jwt.Claims{}, + TenantID: "TENANTID", + PrincipalID: "PRINCIPALID", + }, + }, + want: "", + errWanted: errors.New("template: agent-path:1:38: executing \"agent-path\" at <.NonExistent>: can't evaluate field NonExistent in type azure.agentPathTemplateData"), + }, + { + name: "error building agent ID with invalid path", + args: args{ + td: "example.org", + agentPathTemplate: "/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}", + claims: &MSITokenClaims{ + Claims: jwt.Claims{}, + }, + }, + want: "", + errWanted: errors.New("invalid agent path suffix \"/azure_msi//\": path cannot contain empty segments"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + td := spiffeid.RequireTrustDomainFromString(test.args.td) + agentPathTemplate, _ := agentpathtemplate.Parse(test.args.agentPathTemplate) + got, err := MakeAgentID(td, agentPathTemplate, test.args.claims) + if test.errWanted != nil { + require.EqualError(t, err, test.errWanted.Error()) + return + } + assert.NoError(t, err) + assert.Equal(t, test.want, got.String()) + }) + } +} + func fakeTokenHTTPClient(statusCode int, body string) HTTPClient { return HTTPClientFunc(func(req *http.Request) (*http.Response, error) { // assert the expected request values diff --git a/pkg/common/protoutil/masks_test.go b/pkg/common/protoutil/masks_test.go index ff712165c5..2860cb6767 100644 --- a/pkg/common/protoutil/masks_test.go +++ b/pkg/common/protoutil/masks_test.go @@ -29,7 +29,8 @@ func TestAllTrueMasks(t *testing.T) { SpiffeId: true, ParentId: true, Selectors: true, - Ttl: true, + X509SvidTtl: true, + JwtSvidTtl: true, FederatesWith: true, Admin: true, Downstream: true, diff --git a/pkg/common/sddl/sddl_windows.go b/pkg/common/sddl/sddl_windows.go index cd28de897f..5bd91390b8 100644 --- a/pkg/common/sddl/sddl_windows.go +++ b/pkg/common/sddl/sddl_windows.go @@ -11,6 +11,14 @@ const ( // to the creator owner only. PrivateFile = "D:P(A;;FA;;;OW)" + // PubliclyReadableFile describes a security descriptor using + // the security descriptor definition language (SDDL) that is meant + // to be used to define the access control to files that need to + // be publicly readable but writable only by the owner of the file. + // The security descriptor grants full access to the creator owner + // and read access to everyone. + PubliclyReadableFile = "D:P(A;;FA;;;OW)(A;;FR;;;WD)" + // PrivateListener describes a security descriptor using the // security descriptor definition language (SDDL) that is meant // to be used to define the access control to named pipes diff --git a/pkg/common/telemetry/names.go b/pkg/common/telemetry/names.go index 5ab5c38b9d..78f289a03f 100644 --- a/pkg/common/telemetry/names.go +++ b/pkg/common/telemetry/names.go @@ -192,6 +192,9 @@ const ( // CGroupPath tags a linux CGroup path, most likely for use in attestation CGroupPath = "cgroup_path" + // Check tags a health check subsystem + Check = "check" + // Connection functionality related to some connection; should be used with other tags // to add clarity Connection = "connection" @@ -203,6 +206,9 @@ const ( // ContainerID tags some container ID, most likely for use in attestation ContainerID = "container_id" + // ContainerName tags some container name, most likely for use in attestation + ContainerName = "container_name" + // Count tags some basic count; should be used with other tags and clear messaging to add clarity Count = "count" @@ -221,6 +227,12 @@ const ( // DeprecatedServiceName tags the deprecated service name DeprecatedServiceName = "deprecated_service_name" + // Details tags details response from a health check subsystem + Details = "details" + + // Duration is the amount of seconds that an error is active + Duration = "duration" + // DiscoveredSelectors tags selectors for some registration DiscoveredSelectors = "discovered_selectors" @@ -266,6 +278,9 @@ const ( // External tag something as external (e.g. external plugin) External = "external" + // Failures amount of concatenated errors + Failures = "failures" + // FederatedAdded labels some count of federated bundles that have been added to an entity FederatedAdded = "fed_add" @@ -473,6 +488,14 @@ const ( // with other tags to add clarity TTL = "ttl" + // X509 SVID TTL functionality related to a time-to-live field for X509-SVIDs; should be used + // with other tags to add clarity + X509SVIDTTL = "x509_svid_ttl" + + // JWT SVID TTL functionality related to a time-to-live field for JWT-SVIDs; should be used + // with other tags to add clarity + JWTSVIDTTL = "jwt_svid_ttl" + // Type tags a type Type = "type" diff --git a/pkg/common/util/sort.go b/pkg/common/util/sort.go index ff983cb4d0..7e0ec903bc 100644 --- a/pkg/common/util/sort.go +++ b/pkg/common/util/sort.go @@ -59,10 +59,15 @@ func compareRegistrationEntries(a, b *common.RegistrationEntry) int { return c } + // The order of this switch clause matters. It ensures that sorting occurs by X509SvidTtl then JwtSvidTtl switch { - case a.Ttl < b.Ttl: + case a.X509SvidTtl < b.X509SvidTtl: return -1 - case a.Ttl > b.Ttl: + case a.X509SvidTtl > b.X509SvidTtl: + return 1 + case a.JwtSvidTtl < b.JwtSvidTtl: + return -1 + case a.JwtSvidTtl > b.JwtSvidTtl: return 1 } @@ -133,10 +138,15 @@ func compareTypesEntries(a, b *types.Entry) int { return c } + // The order of this switch clause matters. It ensures that sorting occurs by X509SvidTtl then JwtSvidTtl switch { - case a.Ttl < b.Ttl: + case a.X509SvidTtl < b.X509SvidTtl: + return -1 + case a.X509SvidTtl > b.X509SvidTtl: + return 1 + case a.JwtSvidTtl < b.JwtSvidTtl: return -1 - case a.Ttl > b.Ttl: + case a.JwtSvidTtl > b.JwtSvidTtl: return 1 } diff --git a/pkg/common/util/sort_test.go b/pkg/common/util/sort_test.go index 8fe8a37031..0d3bff25ee 100644 --- a/pkg/common/util/sort_test.go +++ b/pkg/common/util/sort_test.go @@ -34,49 +34,56 @@ func TestDedupRegistrationEntries(t *testing.T) { func TestSortRegistrationEntries(t *testing.T) { entries := []*common.RegistrationEntry{ // entries to assert that spiffe ids are compared for sorting first - {SpiffeId: "a", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "b", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "c", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "a", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "b", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "c", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, // entries to assert that parent ids are compared for sorting second - {SpiffeId: "x", ParentId: "a", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "b", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "c", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - // entries to assert that ttl is compared for sorting third - {SpiffeId: "x", ParentId: "x", Ttl: 10, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 20, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 30, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - // entries to assert that selector types are compared for sorting fourth - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "a", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "b", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "c", Value: "x"}}}, + {SpiffeId: "x", ParentId: "a", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "b", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "c", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + // entries to assert that x509SvidTtl is compared for sorting third + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 10, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 20, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 30, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + // entries to assert that jwtSvidTtl is compared for sorting fourth + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 10, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 20, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 30, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + // entries to assert that selector types are compared for sorting fifth + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "a", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "b", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "c", Value: "x"}}}, // entries to assert that selector values are included in selector sorting - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "a"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "b"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "c"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "a"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "b"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "c"}}}, // entry to assert that entries with more selectors come after entries with less - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, // entry to assert that selectors get sorted as well - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "a", Value: "c"}, {Type: "a", Value: "a"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "a", Value: "c"}, {Type: "a", Value: "a"}}}, } expected := []*common.RegistrationEntry{ - {SpiffeId: "a", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "b", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "c", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "a", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "b", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "c", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 10, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 20, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 30, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "a", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "b", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "c", Value: "x"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "a"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "b"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "x", Value: "c"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, - {SpiffeId: "x", ParentId: "x", Ttl: 90, Selectors: []*common.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "c"}}}, + {SpiffeId: "a", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "b", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "c", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "a", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "b", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "c", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 10, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 20, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 30, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 10, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 20, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 30, Selectors: []*common.Selector{{Type: "x", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "a", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "b", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "c", Value: "x"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "a"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "b"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "x", Value: "c"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, + {SpiffeId: "x", ParentId: "x", X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*common.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "c"}}}, } var actual []*common.RegistrationEntry @@ -122,49 +129,57 @@ func TestSortTypesEntries(t *testing.T) { entries := []*types.Entry{ // entries to assert that spiffe ids are compared for sorting first - {SpiffeId: idA, ParentId: idX, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: idB, ParentId: idX, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: idC, ParentId: idX, Ttl: 90, Selectors: selectorsX}, + {SpiffeId: idA, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idB, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idC, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, // entries to assert that parent ids are compared for sorting second - {SpiffeId: idX, ParentId: idA, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idB, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idC, Ttl: 90, Selectors: selectorsX}, - // entries to assert that ttl is compared for sorting third - {SpiffeId: idX, ParentId: idX, Ttl: 10, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idX, Ttl: 20, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idX, Ttl: 30, Selectors: selectorsX}, - // entries to assert that selector types are compared for sorting fourth - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "a", Value: "x"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "b", Value: "x"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "c", Value: "x"}}}, + {SpiffeId: idX, ParentId: idA, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idB, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idC, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + // entries to assert that x509SvidTtl is compared for sorting third + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 10, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 20, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 30, JwtSvidTtl: 110, Selectors: selectorsX}, + // entries to assert that jwtSvidTtl is compared for sorting forth + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 10, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 20, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 30, Selectors: selectorsX}, + + // entries to assert that selector types are compared for sorting fifth + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "a", Value: "x"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "b", Value: "x"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "c", Value: "x"}}}, // entries to assert that selector values are included in selector sorting - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "x", Value: "a"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "x", Value: "b"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "x", Value: "c"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "x", Value: "a"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "x", Value: "b"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "x", Value: "c"}}}, // entry to assert that entries with more selectors come after entries with less - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, // entry to assert that selectors get sorted as well - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "a", Value: "c"}, {Type: "a", Value: "a"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "a", Value: "c"}, {Type: "a", Value: "a"}}}, } expected := []*types.Entry{ - {SpiffeId: &types.SPIFFEID{TrustDomain: "a"}, ParentId: &types.SPIFFEID{TrustDomain: "x"}, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: &types.SPIFFEID{TrustDomain: "b"}, ParentId: &types.SPIFFEID{TrustDomain: "x"}, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: &types.SPIFFEID{TrustDomain: "c"}, ParentId: &types.SPIFFEID{TrustDomain: "x"}, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: &types.SPIFFEID{TrustDomain: "x"}, ParentId: &types.SPIFFEID{TrustDomain: "a"}, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: &types.SPIFFEID{TrustDomain: "x"}, ParentId: &types.SPIFFEID{TrustDomain: "b"}, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: &types.SPIFFEID{TrustDomain: "x"}, ParentId: &types.SPIFFEID{TrustDomain: "c"}, Ttl: 90, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idX, Ttl: 10, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idX, Ttl: 20, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idX, Ttl: 30, Selectors: selectorsX}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "a", Value: "x"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "b", Value: "x"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "c", Value: "x"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "x", Value: "a"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "x", Value: "b"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "x", Value: "c"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, - {SpiffeId: idX, ParentId: idX, Ttl: 90, Selectors: []*types.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "c"}}}, + {SpiffeId: &types.SPIFFEID{TrustDomain: "a"}, ParentId: &types.SPIFFEID{TrustDomain: "x"}, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: &types.SPIFFEID{TrustDomain: "b"}, ParentId: &types.SPIFFEID{TrustDomain: "x"}, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: &types.SPIFFEID{TrustDomain: "c"}, ParentId: &types.SPIFFEID{TrustDomain: "x"}, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: &types.SPIFFEID{TrustDomain: "x"}, ParentId: &types.SPIFFEID{TrustDomain: "a"}, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: &types.SPIFFEID{TrustDomain: "x"}, ParentId: &types.SPIFFEID{TrustDomain: "b"}, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: &types.SPIFFEID{TrustDomain: "x"}, ParentId: &types.SPIFFEID{TrustDomain: "c"}, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 10, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 20, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 30, JwtSvidTtl: 110, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 10, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 20, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 30, Selectors: selectorsX}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "a", Value: "x"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "b", Value: "x"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "c", Value: "x"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "x", Value: "a"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "x", Value: "b"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "x", Value: "c"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "b"}}}, + {SpiffeId: idX, ParentId: idX, X509SvidTtl: 100, JwtSvidTtl: 110, Selectors: []*types.Selector{{Type: "a", Value: "a"}, {Type: "a", Value: "c"}}}, } var actual []*types.Entry diff --git a/pkg/common/version/version.go b/pkg/common/version/version.go index 38a42cb492..be476d0edf 100644 --- a/pkg/common/version/version.go +++ b/pkg/common/version/version.go @@ -8,7 +8,7 @@ const ( // IMPORTANT: When updating, make sure to reconcile the versions list that // is part of the upgrade integration test. See // test/integration/suites/upgrade/README.md for details. - Base = "1.5.0" + Base = "1.5.4" ) var ( diff --git a/pkg/server/api/agent/v1/service_test.go b/pkg/server/api/agent/v1/service_test.go index 90c808519b..2d587c2e2b 100644 --- a/pkg/server/api/agent/v1/service_test.go +++ b/pkg/server/api/agent/v1/service_test.go @@ -31,7 +31,6 @@ import ( "github.com/spiffe/spire/test/fakes/fakeservernodeattestor" "github.com/spiffe/spire/test/spiretest" "github.com/spiffe/spire/test/testkey" - "github.com/spiffe/spire/test/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -50,6 +49,7 @@ var ( ctx = context.Background() td = spiffeid.RequireTrustDomainFromString("example.org") agentID = spiffeid.RequireFromPath(td, "/agent") + testKey = testkey.MustEC256() testNodes = map[string]*common.AttestedNode{ agent1: { @@ -1597,7 +1597,6 @@ func TestGetAgent(t *testing.T) { } func TestRenewAgent(t *testing.T) { - testKey := testkey.MustEC256() agentIDType := &types.SPIFFEID{TrustDomain: "example.org", Path: "/agent"} defaultNode := &common.AttestedNode{ @@ -2137,11 +2136,7 @@ func TestCreateJoinTokenWithAgentId(t *testing.T) { } func TestAttestAgent(t *testing.T) { - util.SkipFlakyTestUnderRaceDetectorWithFiledIssue( - t, - "https://github.com/spiffe/spire/issues/2841", - ) - testCsr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, testkey.MustEC256()) + testCsr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, testKey) require.NoError(t, err) _, expectedCsrErr := x509.ParseCertificateRequest([]byte("not a csr")) @@ -2998,7 +2993,28 @@ func TestAttestAgent(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // setup test := setupServiceTest(t, 0) - defer test.Cleanup() + defer func() { + // Since this is a bidirectional streaming API, it's possible + // that the server is still emitting auditing logs even though + // we've received the last response from the server. In order + // to avoid racing on the log hook, clean up the test (to make + // sure the server has shut down) before checking for log + // entries. + test.Cleanup() + + // Scrub out client address before comparing logs. + for _, e := range test.logHook.AllEntries() { + if _, ok := e.Data[telemetry.Address]; ok { + e.Data[telemetry.Address] = "" + } + } + if tt.retry { + // Prevent cases where audit logs from previous calls are pushed after log is reset + spiretest.AssertLastLogs(t, test.logHook.AllEntries(), tt.expectLogs) + } else { + spiretest.AssertLogs(t, test.logHook.AllEntries(), tt.expectLogs) + } + }() ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -3040,18 +3056,10 @@ func TestAttestAgent(t *testing.T) { case tt.expectCode != codes.OK: require.Nil(t, result) default: - // Clean address on logs - for _, e := range test.logHook.AllEntries() { - if _, ok := e.Data[telemetry.Address]; ok { - e.Data[telemetry.Address] = "" - } - } - require.NotNil(t, result) test.assertAttestAgentResult(t, tt.expectedID, result) test.assertAgentWasStored(t, tt.expectedID.String(), tt.expectedSelectors) } - spiretest.AssertLogs(t, test.logHook.AllEntries(), tt.expectLogs) }) } } diff --git a/pkg/server/api/bundle/v1/service_test.go b/pkg/server/api/bundle/v1/service_test.go index 793c6c66f4..f19d5405dc 100644 --- a/pkg/server/api/bundle/v1/service_test.go +++ b/pkg/server/api/bundle/v1/service_test.go @@ -791,10 +791,11 @@ func TestBatchDeleteFederatedBundle(t *testing.T) { td3.IDString(), } newEntry := &common.RegistrationEntry{ - EntryId: "entry1", - ParentId: "spiffe://example.org/foo", - SpiffeId: "spiffe://example.org/bar", - Ttl: 60, + EntryId: "entry1", + ParentId: "spiffe://example.org/foo", + SpiffeId: "spiffe://example.org/bar", + X509SvidTtl: 70, + JwtSvidTtl: 80, Selectors: []*common.Selector{ {Type: "a", Value: "1"}, }, diff --git a/pkg/server/api/entry.go b/pkg/server/api/entry.go index 087100ef36..e1c8bfc292 100644 --- a/pkg/server/api/entry.go +++ b/pkg/server/api/entry.go @@ -61,7 +61,7 @@ func RegistrationEntryToProto(e *common.RegistrationEntry) (*types.Entry, error) SpiffeId: ProtoFromID(spiffeID), ParentId: ProtoFromID(parentID), Selectors: ProtoFromSelectors(e.Selectors), - Ttl: e.Ttl, + X509SvidTtl: e.X509SvidTtl, FederatesWith: federatesWith, Admin: e.Admin, Downstream: e.Downstream, @@ -69,6 +69,7 @@ func RegistrationEntryToProto(e *common.RegistrationEntry) (*types.Entry, error) DnsNames: append([]string(nil), e.DnsNames...), RevisionNumber: e.RevisionNumber, StoreSvid: e.StoreSvid, + JwtSvidTtl: e.JwtSvidTtl, }, nil } @@ -156,11 +157,6 @@ func ProtoToRegistrationEntryWithMask(ctx context.Context, td spiffeid.TrustDoma } } - var ttl int32 - if mask.Ttl { - ttl = e.Ttl - } - var revisionNumber int64 if mask.RevisionNumber { revisionNumber = e.RevisionNumber @@ -171,6 +167,16 @@ func ProtoToRegistrationEntryWithMask(ctx context.Context, td spiffeid.TrustDoma storeSVID = e.StoreSvid } + var x509SvidTTL int32 + if mask.X509SvidTtl { + x509SvidTTL = e.X509SvidTtl + } + + var jwtSvidTTL int32 + if mask.JwtSvidTtl { + jwtSvidTTL = e.JwtSvidTtl + } + return &common.RegistrationEntry{ EntryId: e.Id, ParentId: parentID.String(), @@ -181,8 +187,9 @@ func ProtoToRegistrationEntryWithMask(ctx context.Context, td spiffeid.TrustDoma EntryExpiry: expiresAt, FederatesWith: federatesWith, Selectors: selectors, - Ttl: ttl, RevisionNumber: revisionNumber, StoreSvid: storeSVID, + X509SvidTtl: x509SvidTTL, + JwtSvidTtl: jwtSvidTTL, }, nil } diff --git a/pkg/server/api/entry/v1/service.go b/pkg/server/api/entry/v1/service.go index 3b965db371..794dc40f82 100644 --- a/pkg/server/api/entry/v1/service.go +++ b/pkg/server/api/entry/v1/service.go @@ -350,10 +350,6 @@ func applyMask(e *types.Entry, mask *types.EntryMask) { e.Selectors = nil } - if !mask.Ttl { - e.Ttl = 0 - } - if !mask.FederatesWith { e.FederatesWith = nil } @@ -381,6 +377,14 @@ func applyMask(e *types.Entry, mask *types.EntryMask) { if !mask.StoreSvid { e.StoreSvid = false } + + if !mask.X509SvidTtl { + e.X509SvidTtl = 0 + } + + if !mask.JwtSvidTtl { + e.JwtSvidTtl = 0 + } } func (s *Service) updateEntry(ctx context.Context, e *types.Entry, inputMask *types.EntryMask, outputMask *types.EntryMask) *entryv1.BatchUpdateEntryResponse_Result { @@ -399,7 +403,6 @@ func (s *Service) updateEntry(ctx context.Context, e *types.Entry, inputMask *ty mask = &common.RegistrationEntryMask{ SpiffeId: inputMask.SpiffeId, ParentId: inputMask.ParentId, - Ttl: inputMask.Ttl, FederatesWith: inputMask.FederatesWith, Admin: inputMask.Admin, Downstream: inputMask.Downstream, @@ -407,6 +410,8 @@ func (s *Service) updateEntry(ctx context.Context, e *types.Entry, inputMask *ty DnsNames: inputMask.DnsNames, Selectors: inputMask.Selectors, StoreSvid: inputMask.StoreSvid, + X509SvidTtl: inputMask.X509SvidTtl, + JwtSvidTtl: inputMask.JwtSvidTtl, } } dsEntry, err := s.ds.UpdateRegistrationEntry(ctx, convEntry, mask) @@ -462,8 +467,12 @@ func fieldsFromEntryProto(ctx context.Context, proto *types.Entry, inputMask *ty } } - if inputMask == nil || inputMask.Ttl { - fields[telemetry.TTL] = proto.Ttl + if inputMask == nil || inputMask.X509SvidTtl { + fields[telemetry.X509SVIDTTL] = proto.X509SvidTtl + } + + if inputMask == nil || inputMask.JwtSvidTtl { + fields[telemetry.JWTSVIDTTL] = proto.JwtSvidTtl } if inputMask == nil || inputMask.FederatesWith { diff --git a/pkg/server/api/entry/v1/service_test.go b/pkg/server/api/entry/v1/service_test.go index d002e24d2c..94bcf59f3e 100644 --- a/pkg/server/api/entry/v1/service_test.go +++ b/pkg/server/api/entry/v1/service_test.go @@ -1198,9 +1198,9 @@ func TestGetEntry(t *testing.T) { entry1SpiffeID := spiffeid.RequireFromSegments(td, "bar") expiresAt := time.Now().Unix() goodEntry, err := ds.CreateRegistrationEntry(ctx, &common.RegistrationEntry{ - ParentId: parent.String(), - SpiffeId: entry1SpiffeID.String(), - Ttl: 60, + ParentId: parent.String(), + SpiffeId: entry1SpiffeID.String(), + X509SvidTtl: 60, Selectors: []*common.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -1263,10 +1263,10 @@ func TestGetEntry(t *testing.T) { name: "no outputMask", entryID: goodEntry.EntryId, expectEntry: &types.Entry{ - Id: goodEntry.EntryId, - ParentId: api.ProtoFromID(parent), - SpiffeId: api.ProtoFromID(entry1SpiffeID), - Ttl: 60, + Id: goodEntry.EntryId, + ParentId: api.ProtoFromID(parent), + SpiffeId: api.ProtoFromID(entry1SpiffeID), + X509SvidTtl: 60, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -1441,9 +1441,9 @@ func TestBatchCreateEntry(t *testing.T) { useDefaultEntryID := "DEFAULT_ENTRY_ID" defaultEntry := &common.RegistrationEntry{ - ParentId: entryParentID.String(), - SpiffeId: entrySpiffeID.String(), - Ttl: 60, + ParentId: entryParentID.String(), + SpiffeId: entrySpiffeID.String(), + X509SvidTtl: 60, Selectors: []*common.Selector{ {Type: "unix", Value: "gid:1000"}, {Type: "unix", Value: "uid:1000"}, @@ -1469,7 +1469,8 @@ func TestBatchCreateEntry(t *testing.T) { Downstream: true, ExpiresAt: expiresAt, FederatesWith: []string{"domain1.org"}, - Ttl: 60, + X509SvidTtl: 45, + JwtSvidTtl: 30, } // Registration entry for test entry testDSEntry := &common.RegistrationEntry{ @@ -1485,7 +1486,8 @@ func TestBatchCreateEntry(t *testing.T) { Downstream: true, EntryExpiry: expiresAt, FederatesWith: []string{"spiffe://domain1.org"}, - Ttl: 60, + X509SvidTtl: 45, + JwtSvidTtl: 30, } for _, tt := range []struct { @@ -1521,7 +1523,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.Selectors: "type:value1,type:value2", telemetry.RevisionNumber: "0", telemetry.SPIFFEID: "spiffe://example.org/workload", - telemetry.TTL: "60", + telemetry.X509SVIDTTL: "45", + telemetry.JWTSVIDTTL: "30", telemetry.StoreSvid: "false", }, }, @@ -1547,7 +1550,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "type:value", telemetry.SPIFFEID: "spiffe://example.org/malformed", - telemetry.TTL: "0", + telemetry.X509SVIDTTL: "0", + telemetry.JWTSVIDTTL: "0", telemetry.StoreSvid: "false", }, }, @@ -1565,7 +1569,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "type:value", telemetry.SPIFFEID: "spiffe://example.org/workload2", - telemetry.TTL: "0", + telemetry.X509SVIDTTL: "0", + telemetry.JWTSVIDTTL: "0", telemetry.StoreSvid: "false", }, }, @@ -1657,7 +1662,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.Selectors: "type:value1,type:value2", telemetry.RevisionNumber: "0", telemetry.SPIFFEID: "spiffe://example.org/svidstore", - telemetry.TTL: "0", + telemetry.X509SVIDTTL: "0", + telemetry.JWTSVIDTTL: "0", telemetry.StoreSvid: "true", }, }, @@ -1715,7 +1721,8 @@ func TestBatchCreateEntry(t *testing.T) { Downstream: true, ExpiresAt: expiresAt, FederatesWith: []string{"domain1.org"}, - Ttl: 60, + X509SvidTtl: 45, + JwtSvidTtl: 30, StoreSvid: false, }, }, @@ -1739,7 +1746,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "type:value1,type:value2", telemetry.SPIFFEID: "spiffe://example.org/workload", - telemetry.TTL: "60", + telemetry.X509SVIDTTL: "45", + telemetry.JWTSVIDTTL: "30", telemetry.StoreSvid: "false", }, }, @@ -1775,7 +1783,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "type:value1,type:value2", telemetry.SPIFFEID: "spiffe://example.org/workload", - telemetry.TTL: "60", + telemetry.X509SVIDTTL: "45", + telemetry.JWTSVIDTTL: "30", telemetry.StoreSvid: "false", }, }, @@ -1804,10 +1813,11 @@ func TestBatchCreateEntry(t *testing.T) { }, reqEntries: []*types.Entry{ { - Id: "entry1", - ParentId: api.ProtoFromID(entryParentID), - SpiffeId: api.ProtoFromID(entrySpiffeID), - Ttl: 60, + Id: "entry1", + ParentId: api.ProtoFromID(entryParentID), + SpiffeId: api.ProtoFromID(entrySpiffeID), + X509SvidTtl: 45, + JwtSvidTtl: 30, Selectors: []*types.Selector{ {Type: "type", Value: "value1"}, }, @@ -1815,10 +1825,11 @@ func TestBatchCreateEntry(t *testing.T) { }, expectDsEntries: map[string]*common.RegistrationEntry{ "entry1": { - EntryId: "entry1", - ParentId: "spiffe://example.org/foo", - SpiffeId: "spiffe://example.org/bar", - Ttl: 60, + EntryId: "entry1", + ParentId: "spiffe://example.org/foo", + SpiffeId: "spiffe://example.org/bar", + X509SvidTtl: 45, + JwtSvidTtl: 30, Selectors: []*common.Selector{ {Type: "type", Value: "value1"}, }, @@ -1839,7 +1850,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "type:value1", telemetry.SPIFFEID: "spiffe://example.org/bar", - telemetry.TTL: "60", + telemetry.X509SVIDTTL: "45", + telemetry.JWTSVIDTTL: "30", telemetry.StoreSvid: "false", }, }, @@ -1866,10 +1878,11 @@ func TestBatchCreateEntry(t *testing.T) { }, reqEntries: []*types.Entry{ { - ParentId: api.ProtoFromID(entryParentID), - SpiffeId: api.ProtoFromID(entrySpiffeID), - Ttl: 20, - Admin: false, + ParentId: api.ProtoFromID(entryParentID), + SpiffeId: api.ProtoFromID(entrySpiffeID), + X509SvidTtl: 45, + JwtSvidTtl: 30, + Admin: false, Selectors: []*types.Selector{ {Type: "unix", Value: "gid:1000"}, {Type: "unix", Value: "uid:1000"}, @@ -1890,7 +1903,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.Selectors: "unix:gid:1000,unix:uid:1000", telemetry.RevisionNumber: "0", telemetry.SPIFFEID: "spiffe://example.org/bar", - telemetry.TTL: "20", + telemetry.X509SVIDTTL: "45", + telemetry.JWTSVIDTTL: "30", telemetry.StatusCode: "AlreadyExists", telemetry.StatusMessage: "similar entry already exists", telemetry.StoreSvid: "false", @@ -1927,7 +1941,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.Downstream: "false", telemetry.ExpiresAt: "0", telemetry.RevisionNumber: "0", - telemetry.TTL: "0", + telemetry.X509SVIDTTL: "0", + telemetry.JWTSVIDTTL: "0", telemetry.StoreSvid: "false", telemetry.StatusCode: "InvalidArgument", telemetry.StatusMessage: "failed to convert entry: invalid parent ID: trust domain is missing", @@ -1967,7 +1982,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "type:value1,type:value2", telemetry.SPIFFEID: "spiffe://example.org/workload", - telemetry.TTL: "60", + telemetry.X509SVIDTTL: "45", + telemetry.JWTSVIDTTL: "30", telemetry.StoreSvid: "false", telemetry.StatusCode: "Internal", telemetry.StatusMessage: "failed to create entry: creating error", @@ -2016,7 +2032,8 @@ func TestBatchCreateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "type:value1,type:value2", telemetry.SPIFFEID: "spiffe://example.org/workload", - telemetry.TTL: "60", + telemetry.X509SVIDTTL: "45", + telemetry.JWTSVIDTTL: "30", telemetry.StoreSvid: "false", telemetry.StatusCode: "Internal", telemetry.StatusMessage: "failed to convert entry: invalid SPIFFE ID: scheme is missing or invalid", @@ -2337,10 +2354,10 @@ func TestBatchDeleteEntry(t *testing.T) { func TestGetAuthorizedEntries(t *testing.T) { entry1 := types.Entry{ - Id: "entry-1", - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, - Ttl: 60, + Id: "entry-1", + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, + X509SvidTtl: 60, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -2355,10 +2372,10 @@ func TestGetAuthorizedEntries(t *testing.T) { Downstream: true, } entry2 := types.Entry{ - Id: "entry-2", - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/baz"}, - Ttl: 3600, + Id: "entry-2", + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/baz"}, + X509SvidTtl: 3600, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1001"}, {Type: "unix", Value: "gid:1001"}, @@ -2641,9 +2658,9 @@ func TestBatchUpdateEntry(t *testing.T) { entry1SpiffeID := &types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"} expiresAt := time.Now().Unix() initialEntry := &types.Entry{ - ParentId: parent, - SpiffeId: entry1SpiffeID, - Ttl: 60, + ParentId: parent, + SpiffeId: entry1SpiffeID, + X509SvidTtl: 60, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "uid:2000"}, @@ -2657,10 +2674,10 @@ func TestBatchUpdateEntry(t *testing.T) { Downstream: true, } storeSvidEntry := &types.Entry{ - ParentId: parent, - SpiffeId: entry1SpiffeID, - Ttl: 60, - StoreSvid: true, + ParentId: parent, + SpiffeId: entry1SpiffeID, + X509SvidTtl: 60, + StoreSvid: true, Selectors: []*types.Selector{ {Type: "typ", Value: "key1:value"}, {Type: "typ", Value: "key2:value"}, @@ -2671,9 +2688,10 @@ func TestBatchUpdateEntry(t *testing.T) { ExpiresAt: expiresAt, } updateEverythingEntry := &types.Entry{ - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, - Ttl: 500000, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, + X509SvidTtl: 400000, + JwtSvidTtl: 300000, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:9999"}, }, @@ -2978,23 +2996,23 @@ func TestBatchUpdateEntry(t *testing.T) { }, }, { - name: "Success Update TTL", + name: "Success Update X509SVIDTTL", initialEntries: []*types.Entry{initialEntry}, inputMask: &types.EntryMask{ - Ttl: true, + X509SvidTtl: true, }, outputMask: &types.EntryMask{ - Ttl: true, + X509SvidTtl: true, }, updateEntries: []*types.Entry{ { - Ttl: 1000, + X509SvidTtl: 1000, }, }, expectDsEntries: func(id string) []*types.Entry { modifiedEntry := proto.Clone(initialEntry).(*types.Entry) modifiedEntry.Id = id - modifiedEntry.Ttl = 1000 + modifiedEntry.X509SvidTtl = 1000 modifiedEntry.RevisionNumber = 1 return []*types.Entry{modifiedEntry} }, @@ -3002,7 +3020,7 @@ func TestBatchUpdateEntry(t *testing.T) { { Status: &types.Status{Code: int32(codes.OK), Message: "OK"}, Entry: &types.Entry{ - Ttl: 1000, + X509SvidTtl: 1000, }, }, }, @@ -3015,7 +3033,7 @@ func TestBatchUpdateEntry(t *testing.T) { telemetry.Status: "success", telemetry.Type: "audit", telemetry.RegistrationID: m[entry1SpiffeID.Path], - telemetry.TTL: "1000", + telemetry.X509SVIDTTL: "1000", }, }, } @@ -3241,17 +3259,17 @@ func TestBatchUpdateEntry(t *testing.T) { }, }, { - name: "Success Don't Update TTL", + name: "Success Don't Update X509SVIDTTL", initialEntries: []*types.Entry{initialEntry}, inputMask: &types.EntryMask{ // With this empty, the update operation should be a no-op }, outputMask: &types.EntryMask{ - Ttl: true, + X509SvidTtl: true, }, updateEntries: []*types.Entry{ { - Ttl: 500000, + X509SvidTtl: 500000, }, }, expectDsEntries: func(m string) []*types.Entry { @@ -3264,7 +3282,7 @@ func TestBatchUpdateEntry(t *testing.T) { { Status: &types.Status{Code: int32(codes.OK), Message: "OK"}, Entry: &types.Entry{ - Ttl: 60, + X509SvidTtl: 60, }, }, }, @@ -3593,9 +3611,10 @@ func TestBatchUpdateEntry(t *testing.T) { { Status: &types.Status{Code: int32(codes.OK), Message: "OK"}, Entry: &types.Entry{ - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, - Ttl: 500000, + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/validUpdated"}, + X509SvidTtl: 400000, + JwtSvidTtl: 300000, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:9999"}, }, @@ -3625,7 +3644,8 @@ func TestBatchUpdateEntry(t *testing.T) { telemetry.RevisionNumber: "0", telemetry.Selectors: "unix:uid:9999", telemetry.SPIFFEID: "spiffe://example.org/validUpdated", - telemetry.TTL: "500000", + telemetry.X509SVIDTTL: "400000", + telemetry.JWTSVIDTTL: "300000", telemetry.StoreSvid: "false", }, }, @@ -3636,21 +3656,21 @@ func TestBatchUpdateEntry(t *testing.T) { name: "Success Nil Output Mask", initialEntries: []*types.Entry{initialEntry}, inputMask: &types.EntryMask{ - Ttl: true, + X509SvidTtl: true, }, outputMask: nil, updateEntries: []*types.Entry{ { - Ttl: 500000, + X509SvidTtl: 500000, }, }, expectResults: []*entryv1.BatchUpdateEntryResponse_Result{ { Status: &types.Status{Code: int32(codes.OK), Message: "OK"}, Entry: &types.Entry{ - ParentId: parent, - SpiffeId: entry1SpiffeID, - Ttl: 500000, + ParentId: parent, + SpiffeId: entry1SpiffeID, + X509SvidTtl: 500000, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "uid:2000"}, @@ -3675,7 +3695,7 @@ func TestBatchUpdateEntry(t *testing.T) { telemetry.Status: "success", telemetry.Type: "audit", telemetry.RegistrationID: m[entry1SpiffeID.Path], - telemetry.TTL: "500000", + telemetry.X509SVIDTTL: "500000", }, }, } @@ -3724,19 +3744,19 @@ func TestBatchUpdateEntry(t *testing.T) { name: "Success Empty Output Mask", initialEntries: []*types.Entry{initialEntry}, inputMask: &types.EntryMask{ - Ttl: true, + X509SvidTtl: true, }, // With the output mask empty, the update will take place, but the results will be empty outputMask: &types.EntryMask{}, updateEntries: []*types.Entry{ { - Ttl: 500000, + X509SvidTtl: 500000, }, }, expectDsEntries: func(m string) []*types.Entry { modifiedEntry := proto.Clone(initialEntry).(*types.Entry) modifiedEntry.Id = m - modifiedEntry.Ttl = 500000 + modifiedEntry.X509SvidTtl = 500000 modifiedEntry.RevisionNumber = 1 return []*types.Entry{modifiedEntry} }, @@ -3755,7 +3775,7 @@ func TestBatchUpdateEntry(t *testing.T) { telemetry.Status: "success", telemetry.Type: "audit", telemetry.RegistrationID: m[entry1SpiffeID.Path], - telemetry.TTL: "500000", + telemetry.X509SVIDTTL: "500000", }, }, } diff --git a/pkg/server/api/entry_test.go b/pkg/server/api/entry_test.go index 6d319992ef..1ee008d17c 100644 --- a/pkg/server/api/entry_test.go +++ b/pkg/server/api/entry_test.go @@ -26,10 +26,11 @@ func TestRegistrationEntryToProto(t *testing.T) { { name: "success", entry: &common.RegistrationEntry{ - EntryId: "entry1", - ParentId: "spiffe://example.org/foo", - SpiffeId: "spiffe://example.org/bar", - Ttl: 60, + EntryId: "entry1", + ParentId: "spiffe://example.org/foo", + SpiffeId: "spiffe://example.org/bar", + X509SvidTtl: 70, + JwtSvidTtl: 80, Selectors: []*common.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -48,10 +49,11 @@ func TestRegistrationEntryToProto(t *testing.T) { RevisionNumber: 99, }, expectEntry: &types.Entry{ - Id: "entry1", - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, - Ttl: 60, + Id: "entry1", + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, + X509SvidTtl: 70, + JwtSvidTtl: 80, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -118,10 +120,11 @@ func TestProtoToRegistrationEntryWithMask(t *testing.T) { { name: "mask including all fields", entry: &types.Entry{ - Id: "entry1", - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, - Ttl: 60, + Id: "entry1", + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, + X509SvidTtl: 70, + JwtSvidTtl: 80, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -140,10 +143,11 @@ func TestProtoToRegistrationEntryWithMask(t *testing.T) { RevisionNumber: 99, }, expectEntry: &common.RegistrationEntry{ - EntryId: "entry1", - ParentId: "spiffe://example.org/foo", - SpiffeId: "spiffe://example.org/bar", - Ttl: 60, + EntryId: "entry1", + ParentId: "spiffe://example.org/foo", + SpiffeId: "spiffe://example.org/bar", + X509SvidTtl: 70, + JwtSvidTtl: 80, Selectors: []*common.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -168,7 +172,8 @@ func TestProtoToRegistrationEntryWithMask(t *testing.T) { Selectors: []*types.Selector{}, DnsNames: []string{"name1"}, FederatesWith: []string{"domain.test"}, - Ttl: 1, + X509SvidTtl: 2, + JwtSvidTtl: 3, Admin: true, Downstream: true, ExpiresAt: 4, @@ -209,10 +214,11 @@ func TestProtoToRegistrationEntry(t *testing.T) { { name: "success", entry: &types.Entry{ - Id: "entry1", - ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, - Ttl: 60, + Id: "entry1", + ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/foo"}, + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/bar"}, + X509SvidTtl: 70, + JwtSvidTtl: 80, Selectors: []*types.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, @@ -231,10 +237,11 @@ func TestProtoToRegistrationEntry(t *testing.T) { RevisionNumber: 99, }, expectEntry: &common.RegistrationEntry{ - EntryId: "entry1", - ParentId: "spiffe://example.org/foo", - SpiffeId: "spiffe://example.org/bar", - Ttl: 60, + EntryId: "entry1", + ParentId: "spiffe://example.org/foo", + SpiffeId: "spiffe://example.org/bar", + X509SvidTtl: 70, + JwtSvidTtl: 80, Selectors: []*common.Selector{ {Type: "unix", Value: "uid:1000"}, {Type: "unix", Value: "gid:1000"}, diff --git a/pkg/server/api/svid/v1/service.go b/pkg/server/api/svid/v1/service.go index 7aa49cc83c..d2efb88c3a 100644 --- a/pkg/server/api/svid/v1/service.go +++ b/pkg/server/api/svid/v1/service.go @@ -115,11 +115,12 @@ func (s *Service) MintX509SVID(ctx context.Context, req *svidv1.MintX509SVIDRequ } rpccontext.AddRPCAuditFields(ctx, logrus.Fields{ - telemetry.ExpiresAt: x509SVID[0].NotAfter.Unix(), + telemetry.ExpiresAt: x509SVID[0].NotAfter.Format(time.RFC3339), }) rpccontext.AuditRPCWithFields(ctx, commonX509SVIDLogFields) log.WithField(telemetry.Expiration, x509SVID[0].NotAfter.Format(time.RFC3339)). + WithField(telemetry.SerialNumber, x509SVID[0].SerialNumber.String()). WithFields(commonX509SVIDLogFields). Debug("Signed X509 SVID") @@ -254,7 +255,7 @@ func (s *Service) newX509SVID(ctx context.Context, param *svidv1.NewX509SVIDPara SpiffeID: spiffeID, PublicKey: csr.PublicKey, DNSList: entry.DnsNames, - TTL: time.Duration(entry.Ttl) * time.Second, + TTL: time.Duration(entry.X509SvidTtl) * time.Second, }) if err != nil { return &svidv1.BatchNewX509SVIDResponse_Result{ @@ -263,6 +264,8 @@ func (s *Service) newX509SVID(ctx context.Context, param *svidv1.NewX509SVIDPara } log.WithField(telemetry.Expiration, x509Svid[0].NotAfter.Format(time.RFC3339)). + WithField(telemetry.SerialNumber, x509Svid[0].SerialNumber.String()). + WithField(telemetry.RevisionNumber, entry.RevisionNumber). Debug("Signed X509 SVID") return &svidv1.BatchNewX509SVIDResponse_Result{ @@ -338,12 +341,12 @@ func (s *Service) NewJWTSVID(ctx context.Context, req *svidv1.NewJWTSVIDRequest) return nil, api.MakeErr(log, codes.NotFound, "entry not found or not authorized", nil) } - jwtsvid, err := s.mintJWTSVID(ctx, entry.SpiffeId, req.Audience, entry.Ttl) + jwtsvid, err := s.mintJWTSVID(ctx, entry.SpiffeId, req.Audience, entry.JwtSvidTtl) if err != nil { return nil, err } rpccontext.AuditRPCWithFields(ctx, logrus.Fields{ - telemetry.TTL: entry.Ttl, + telemetry.TTL: entry.JwtSvidTtl, }) return &svidv1.NewJWTSVIDResponse{ @@ -377,7 +380,7 @@ func (s *Service) NewDownstreamX509CA(ctx context.Context, req *svidv1.NewDownst x509CASvid, err := s.ca.SignX509CASVID(ctx, ca.X509CASVIDParams{ SpiffeID: s.td.ID(), PublicKey: csr.PublicKey, - TTL: time.Duration(entry.Ttl) * time.Second, + TTL: time.Duration(entry.X509SvidTtl) * time.Second, }) if err != nil { return nil, api.MakeErr(log, codes.Internal, "failed to sign downstream X.509 CA", err) diff --git a/pkg/server/api/svid/v1/service_test.go b/pkg/server/api/svid/v1/service_test.go index ac06da209e..241558359f 100644 --- a/pkg/server/api/svid/v1/service_test.go +++ b/pkg/server/api/svid/v1/service_test.go @@ -54,9 +54,9 @@ func TestServiceMintX509SVID(t *testing.T) { x509CA := test.ca.X509CA() now := test.ca.Clock().Now().UTC() expiredAt := now.Add(test.ca.X509SVIDTTL()) - expiresAtStr := strconv.FormatInt(expiredAt.Unix(), 10) + expiresAtStr := expiredAt.Format(time.RFC3339) customExpiresAt := now.Add(10 * time.Second) - expiresAtCustomStr := strconv.FormatInt(customExpiresAt.Unix(), 10) + expiresAtCustomStr := customExpiresAt.Format(time.RFC3339) for _, tt := range []struct { name string @@ -831,10 +831,17 @@ func TestServiceNewJWTSVID(t *testing.T) { SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/agent"}, } entryWithTTL := &types.Entry{ - Id: "agent-entry-ttl-id", - ParentId: api.ProtoFromID(agentID), - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/agent-ttl"}, - Ttl: 10, + Id: "agent-entry-ttl-id", + ParentId: api.ProtoFromID(agentID), + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/agent-ttl"}, + X509SvidTtl: 10, + } + entryWithJWTTTL := &types.Entry{ + Id: "agent-entry-ttl-id", + ParentId: api.ProtoFromID(agentID), + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/agent-ttl"}, + X509SvidTtl: 30, // ensure this isn't used + JwtSvidTtl: 10, } invalidEntry := &types.Entry{ Id: "invalid-entry", @@ -842,7 +849,7 @@ func TestServiceNewJWTSVID(t *testing.T) { SpiffeId: &types.SPIFFEID{}, } - test.ef.entries = []*types.Entry{entry, entryWithTTL, invalidEntry} + test.ef.entries = []*types.Entry{entry, entryWithTTL, entryWithJWTTTL, invalidEntry} jwtKey := test.ca.JWTKey() now := test.ca.Clock().Now().UTC() @@ -900,6 +907,25 @@ func TestServiceNewJWTSVID(t *testing.T) { }, }, }, + { + name: "success custom JWT TTL", + audience: []string{"AUDIENCE"}, + entry: entryWithJWTTTL, + expiresAt: now.Add(10 * time.Second), + expectLogs: []spiretest.LogEntry{ + { + Level: logrus.InfoLevel, + Message: "API accessed", + Data: logrus.Fields{ + telemetry.Status: "success", + telemetry.Type: "audit", + telemetry.Audience: "AUDIENCE", + telemetry.RegistrationID: "agent-entry-ttl-id", + telemetry.TTL: "10", + }, + }, + }, + }, { name: "no SPIFFE ID", code: codes.InvalidArgument, @@ -1102,7 +1128,7 @@ func TestServiceNewJWTSVID(t *testing.T) { issuedAt, tt.expiresAt, expiresAt, - time.Duration(tt.entry.Ttl)*time.Second) + time.Duration(tt.entry.X509SvidTtl)*time.Second) }) } } @@ -1123,22 +1149,32 @@ func TestServiceBatchNewX509SVID(t *testing.T) { DnsNames: []string{"entryDNS1", "entryDNS2"}, } ttlEntry := &types.Entry{ - Id: "ttl", - ParentId: api.ProtoFromID(agentID), - SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/ttl"}, - Ttl: 10, + Id: "ttl", + ParentId: api.ProtoFromID(agentID), + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/ttl"}, + X509SvidTtl: 10, + JwtSvidTtl: 30, // ensures this is ignored + } + x509TtlEntry := &types.Entry{ + Id: "x509ttl", + ParentId: api.ProtoFromID(agentID), + SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/ttl"}, + X509SvidTtl: 50, + JwtSvidTtl: 30, // ensures this is ignored } invalidEntry := &types.Entry{ Id: "invalid", ParentId: api.ProtoFromID(agentID), } - test.ef.entries = []*types.Entry{workloadEntry, dnsEntry, ttlEntry, invalidEntry} + test.ef.entries = []*types.Entry{workloadEntry, dnsEntry, ttlEntry, x509TtlEntry, invalidEntry} x509CA := test.ca.X509CA() now := test.ca.Clock().Now().UTC() - expiresAtFromTTLEntry := now.Add(time.Duration(ttlEntry.Ttl) * time.Second).Unix() + expiresAtFromTTLEntry := now.Add(time.Duration(ttlEntry.X509SvidTtl) * time.Second).Unix() expiresAtFromTTLEntryStr := strconv.FormatInt(expiresAtFromTTLEntry, 10) + expiresAtFromX509TTLEntry := now.Add(time.Duration(x509TtlEntry.X509SvidTtl) * time.Second).Unix() + expiresAtFromX509TTLEntryStr := strconv.FormatInt(expiresAtFromX509TTLEntry, 10) expiresAtFromCA := now.Add(test.ca.X509SVIDTTL()).Unix() expiresAtFromCAStr := strconv.FormatInt(expiresAtFromCA, 10) @@ -1209,6 +1245,29 @@ func TestServiceBatchNewX509SVID(t *testing.T) { }, } }, + }, { + name: "custom x509 ttl", + reqs: []string{x509TtlEntry.Id}, + expectResults: []*expectResult{ + { + entry: x509TtlEntry, + }, + }, + expectLogs: func(m map[string][]byte) []spiretest.LogEntry { + return []spiretest.LogEntry{ + { + Level: logrus.InfoLevel, + Message: "API accessed", + Data: logrus.Fields{ + telemetry.Status: "success", + telemetry.Type: "audit", + telemetry.RegistrationID: "x509ttl", + telemetry.Csr: api.HashByte(m["x509ttl"]), + telemetry.ExpiresAt: expiresAtFromX509TTLEntryStr, + }, + }, + } + }, }, { name: "custom dns", reqs: []string{dnsEntry.Id}, @@ -1725,8 +1784,8 @@ func TestServiceBatchNewX509SVID(t *testing.T) { // Use entry ttl when defined ttl := test.ca.X509SVIDTTL() - if entry.Ttl != 0 { - ttl = time.Duration(entry.Ttl) * time.Second + if entry.X509SvidTtl != 0 { + ttl = time.Duration(entry.X509SvidTtl) * time.Second } expiresAt := now.Add(ttl) diff --git a/pkg/server/bundle/client/manager_test.go b/pkg/server/bundle/client/manager_test.go index 6d1d390aed..38ccc6ca93 100644 --- a/pkg/server/bundle/client/manager_test.go +++ b/pkg/server/bundle/client/manager_test.go @@ -13,7 +13,6 @@ import ( "github.com/spiffe/spire/pkg/common/telemetry" "github.com/spiffe/spire/test/clock" "github.com/spiffe/spire/test/fakes/fakedatastore" - "github.com/spiffe/spire/test/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeebo/errs" @@ -27,12 +26,12 @@ func TestManagerPeriodicBundleRefresh(t *testing.T) { endpointBundle := bundleutil.BundleFromRootCA(trustDomain, createCACertificate(t, "endpoint")) endpointBundle.SetRefreshHint(time.Hour * 2) - source := TrustDomainConfigMap{ + source := NewTrustDomainConfigSet(TrustDomainConfigMap{ trustDomain: TrustDomainConfig{ EndpointURL: "https://example.org/bundle", EndpointProfile: HTTPSWebProfile{}, }, - } + }) testCases := []struct { name string @@ -85,13 +84,9 @@ func TestManagerPeriodicBundleRefresh(t *testing.T) { } func TestManagerOnDemandBundleRefresh(t *testing.T) { - util.SkipFlakyTestUnderRaceDetectorWithFiledIssue( - t, - "https://github.com/spiffe/spire/issues/2840", - ) - trustDomainConfigs := make(TrustDomainConfigMap) + configSet := NewTrustDomainConfigSet(nil) - test := newManagerTest(t, trustDomainConfigs, nil, nil) + test := newManagerTest(t, configSet, nil, nil) // Wait for the config to be refreshed test.WaitForConfigRefresh() @@ -104,15 +99,19 @@ func TestManagerOnDemandBundleRefresh(t *testing.T) { // Now, add the trust domain configuration to the source and assert // that refreshing the bundle reloads configs from the source. - trustDomainConfigs[trustDomain] = TrustDomainConfig{ + configSet.Set(trustDomain, TrustDomainConfig{ EndpointURL: "https://some-domain.test/bundle", EndpointProfile: HTTPSWebProfile{}, - } + }) has, err = test.RefreshBundleFor(trustDomain) assert.True(t, has, "manager should know about the trust domain") assert.EqualError(t, err, "OHNO") - assert.Equal(t, 1, test.UpdateCount(trustDomain)) + + // The update count may be more than 1, since RefreshBundle will update the + // bundle, but also, since the trust domain is newly managed, kick off a + // goroutine that will refresh it as well. + assert.Greater(t, test.UpdateCount(trustDomain), 0) } func TestManagerConfigPeriodicRefresh(t *testing.T) { @@ -141,11 +140,12 @@ func TestManagerConfigPeriodicRefresh(t *testing.T) { }, } - trustDomainConfigs := make(TrustDomainConfigMap) - trustDomainConfigs[td1] = configSPIFFEA - trustDomainConfigs[td2] = configWebA + configSet := NewTrustDomainConfigSet(TrustDomainConfigMap{ + td1: configSPIFFEA, + td2: configWebA, + }) - test := newManagerTest(t, trustDomainConfigs, nil, nil) + test := newManagerTest(t, configSet, nil, nil) // Wait until the config is refreshed and a bundle refresh happens test.WaitForConfigRefresh() @@ -166,9 +166,10 @@ func TestManagerConfigPeriodicRefresh(t *testing.T) { // Now adjust the configuration to drop td1, change td2, and introduce td3. // Both td2 and td3 should have an extra update count. td1 update count will // remain the same. - delete(trustDomainConfigs, td1) - trustDomainConfigs[td2] = configSPIFFEB - trustDomainConfigs[td3] = configWebB + configSet.SetAll(TrustDomainConfigMap{ + td2: configSPIFFEB, + td3: configWebB, + }) // Wait until the config is refreshed and a bundle refresh happens test.AdvanceTime(bundleutil.MinimumRefreshHint + time.Millisecond) @@ -198,10 +199,11 @@ func TestManagerConfigManualRefresh(t *testing.T) { EndpointProfile: HTTPSWebProfile{}, } - trustDomainConfigs := make(TrustDomainConfigMap) - trustDomainConfigs[td1] = config1 + configSet := NewTrustDomainConfigSet(TrustDomainConfigMap{ + td1: config1, + }) - test := newManagerTest(t, trustDomainConfigs, nil, nil) + test := newManagerTest(t, configSet, nil, nil) // Wait for the original config to be loaded test.WaitForConfigRefresh() @@ -210,7 +212,7 @@ func TestManagerConfigManualRefresh(t *testing.T) { }, test.GetTrustDomainConfigs()) // Update config and trigger the reload - trustDomainConfigs[td2] = config2 + configSet.Set(td2, config2) test.manager.TriggerConfigReload() test.WaitForConfigRefresh() require.Equal(t, map[spiffeid.TrustDomain]TrustDomainConfig{ diff --git a/pkg/server/bundle/client/sources.go b/pkg/server/bundle/client/sources.go index f30223f566..b66ff56fc8 100644 --- a/pkg/server/bundle/client/sources.go +++ b/pkg/server/bundle/client/sources.go @@ -2,6 +2,7 @@ package client import ( "context" + "sync" "github.com/sirupsen/logrus" "github.com/spiffe/go-spiffe/v2/spiffeid" @@ -19,10 +20,45 @@ func (fn TrustDomainConfigSourceFunc) GetTrustDomainConfigs(ctx context.Context) return fn(ctx) } -type TrustDomainConfigMap map[spiffeid.TrustDomain]TrustDomainConfig +type TrustDomainConfigMap = map[spiffeid.TrustDomain]TrustDomainConfig -func (m TrustDomainConfigMap) GetTrustDomainConfigs(ctx context.Context) (map[spiffeid.TrustDomain]TrustDomainConfig, error) { - return m, nil +type TrustDomainConfigSet struct { + mtx sync.RWMutex + configMap TrustDomainConfigMap +} + +func NewTrustDomainConfigSet(configs TrustDomainConfigMap) *TrustDomainConfigSet { + s := &TrustDomainConfigSet{} + s.SetAll(configs) + return s +} + +func (s *TrustDomainConfigSet) Set(td spiffeid.TrustDomain, config TrustDomainConfig) { + s.mtx.Lock() + defer s.mtx.Unlock() + s.configMap[td] = config +} + +func (s *TrustDomainConfigSet) SetAll(configMap TrustDomainConfigMap) { + configMap = duplicateTrustDomainConfigMap(configMap) + + s.mtx.Lock() + defer s.mtx.Unlock() + s.configMap = configMap +} + +func (s *TrustDomainConfigSet) GetTrustDomainConfigs(ctx context.Context) (map[spiffeid.TrustDomain]TrustDomainConfig, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.configMap, nil +} + +func duplicateTrustDomainConfigMap(in TrustDomainConfigMap) TrustDomainConfigMap { + out := make(TrustDomainConfigMap, len(in)) + for td, config := range in { + out[td] = config + } + return out } func MergeTrustDomainConfigSources(sources ...TrustDomainConfigSource) TrustDomainConfigSource { diff --git a/pkg/server/bundle/client/sources_test.go b/pkg/server/bundle/client/sources_test.go index 0dbcead875..ffa498eb45 100644 --- a/pkg/server/bundle/client/sources_test.go +++ b/pkg/server/bundle/client/sources_test.go @@ -21,15 +21,15 @@ var ( ) func TestMergedTrustDomainConfigSource(t *testing.T) { - sourceA := client.TrustDomainConfigMap{ + sourceA := client.NewTrustDomainConfigSet(client.TrustDomainConfigMap{ domain1: client.TrustDomainConfig{EndpointURL: "A"}, - } - sourceB := client.TrustDomainConfigMap{ + }) + sourceB := client.NewTrustDomainConfigSet(client.TrustDomainConfigMap{ domain1: client.TrustDomainConfig{EndpointURL: "B"}, - } - sourceC := client.TrustDomainConfigMap{ + }) + sourceC := client.NewTrustDomainConfigSet(client.TrustDomainConfigMap{ domain2: client.TrustDomainConfig{EndpointURL: "A"}, - } + }) t.Run("context is passed through and error returned", func(t *testing.T) { expectedCtx, cancel := context.WithCancel(context.Background()) diff --git a/pkg/server/ca/journal.go b/pkg/server/ca/journal.go index de939cb111..0df23f84a9 100644 --- a/pkg/server/ca/journal.go +++ b/pkg/server/ca/journal.go @@ -149,7 +149,7 @@ func saveJournalEntries(path string, entries *JournalEntries) error { Bytes: entriesBytes, }) - if err := diskutil.AtomicWriteFile(path, pemBytes, 0644); err != nil { + if err := diskutil.AtomicWritePubliclyReadableFile(path, pemBytes); err != nil { return errs.Wrap(err) } diff --git a/pkg/server/catalog/keymanager.go b/pkg/server/catalog/keymanager.go index e28de3751c..c70f020d41 100644 --- a/pkg/server/catalog/keymanager.go +++ b/pkg/server/catalog/keymanager.go @@ -6,6 +6,7 @@ import ( "github.com/spiffe/spire/pkg/server/plugin/keymanager" "github.com/spiffe/spire/pkg/server/plugin/keymanager/awskms" "github.com/spiffe/spire/pkg/server/plugin/keymanager/disk" + "github.com/spiffe/spire/pkg/server/plugin/keymanager/gcpkms" "github.com/spiffe/spire/pkg/server/plugin/keymanager/memory" ) @@ -29,6 +30,7 @@ func (repo *keyManagerRepository) BuiltIns() []catalog.BuiltIn { return []catalog.BuiltIn{ awskms.BuiltIn(), disk.BuiltIn(), + gcpkms.BuiltIn(), memory.BuiltIn(), } } diff --git a/pkg/server/config.go b/pkg/server/config.go index 1c970acc9c..ee661c9eea 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -59,8 +59,11 @@ type Config struct { // AgentTTL is time-to-live for agent SVIDs AgentTTL time.Duration - // SVIDTTL is default time-to-live for SVIDs - SVIDTTL time.Duration + // X509SVIDTTL is default time-to-live for X509-SVIDs (overrides SVIDTTL) + X509SVIDTTL time.Duration + + // JWTSVIDTTL is default time-to-live for SVIDs (overrides SVIDTTL) + JWTSVIDTTL time.Duration // CATTL is the time-to-live for the server CA. This only applies to // self-signed CA certificates, otherwise it is up to the upstream CA. diff --git a/pkg/server/datastore/sqlstore/migration.go b/pkg/server/datastore/sqlstore/migration.go index 01ee3771f1..67f5c57374 100644 --- a/pkg/server/datastore/sqlstore/migration.go +++ b/pkg/server/datastore/sqlstore/migration.go @@ -134,6 +134,12 @@ import ( // | v1.3.2 | 19 | Added x509_svid_ttl and jwt_svid_ttl columns to entries | // |---------| | | // | v1.3.3 | | | +// |---------| | | +// | v1.3.4 | | | +// |---------| | | +// | v1.3.5 | | | +// |---------| | | +// | v1.3.6 | | | // |*********| | | // | v1.4.0 | | | // |---------| | | @@ -144,6 +150,13 @@ import ( // | v1.4.3 | | | // |---------| | | // | v1.4.4 | | | +// |---------| | | +// | v1.4.5 | | | +// |*********| | | +// | v1.5.0 | | | +// | v1.5.1 | | | +// | v1.5.2 | | | +// | v1.5.3 | | | // ================================================================================================ const ( diff --git a/pkg/server/datastore/sqlstore/models.go b/pkg/server/datastore/sqlstore/models.go index 15fdbdd6ed..2a9f23dda5 100644 --- a/pkg/server/datastore/sqlstore/models.go +++ b/pkg/server/datastore/sqlstore/models.go @@ -76,7 +76,7 @@ type RegisteredEntry struct { EntryID string `gorm:"unique_index"` SpiffeID string `gorm:"index"` ParentID string `gorm:"index"` - // TTL of identities derived from this entry + // TTL of identities derived from this entry. This field represents the X509-SVID TTL of the Entry TTL int32 Selectors []Selector FederatesWith []Bundle `gorm:"many2many:federated_registration_entries;"` @@ -98,9 +98,6 @@ type RegisteredEntry struct { // multiple SVIDs Hint string - // TTL of X509 identities derived from this entry - X509SvidTTL int32 `gorm:"column:x509_svid_ttl"` - // TTL of JWT identities derived from this entry JWTSvidTTL int32 `gorm:"column:jwt_svid_ttl"` } diff --git a/pkg/server/datastore/sqlstore/sqlstore.go b/pkg/server/datastore/sqlstore/sqlstore.go index c4cdc3765d..212249a8bd 100644 --- a/pkg/server/datastore/sqlstore/sqlstore.go +++ b/pkg/server/datastore/sqlstore/sqlstore.go @@ -1782,11 +1782,12 @@ func createRegistrationEntry(tx *gorm.DB, entry *common.RegistrationEntry) (*com EntryID: entryID, SpiffeID: entry.SpiffeId, ParentID: entry.ParentId, - TTL: entry.Ttl, + TTL: entry.X509SvidTtl, Admin: entry.Admin, Downstream: entry.Downstream, Expiry: entry.EntryExpiry, StoreSvid: entry.StoreSvid, + JWTSvidTTL: entry.JwtSvidTtl, } if err := tx.Create(&newRegisteredEntry).Error; err != nil { @@ -1907,7 +1908,8 @@ SELECT NULL AS trust_domain, NULL AS dns_name_id, NULL AS dns_name, - revision_number + revision_number, + jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries WHERE id IN (SELECT id FROM listing) @@ -1915,7 +1917,7 @@ WHERE id IN (SELECT id FROM listing) UNION SELECT - F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL + F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL, NULL FROM bundles B INNER JOIN @@ -1928,7 +1930,7 @@ WHERE UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL, NULL FROM dns_names WHERE registered_entry_id IN (SELECT id FROM listing) @@ -1936,7 +1938,7 @@ WHERE registered_entry_id IN (SELECT id FROM listing) UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL, NULL FROM selectors WHERE registered_entry_id IN (SELECT id FROM listing) @@ -1967,7 +1969,8 @@ SELECT NULL AS trust_domain, NULL ::integer AS dns_name_id, NULL AS dns_name, - revision_number + revision_number, + jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries WHERE id IN (SELECT id FROM listing) @@ -1975,7 +1978,7 @@ WHERE id IN (SELECT id FROM listing) UNION SELECT - F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL + F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL, NULL FROM bundles B INNER JOIN @@ -1988,7 +1991,7 @@ WHERE UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL, NULL FROM dns_names WHERE registered_entry_id IN (SELECT id FROM listing) @@ -1996,7 +1999,7 @@ WHERE registered_entry_id IN (SELECT id FROM listing) UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL, NULL FROM selectors WHERE registered_entry_id IN (SELECT id FROM listing) @@ -2024,7 +2027,8 @@ SELECT B.trust_domain, D.id AS dns_name_id, D.value AS dns_name, - E.revision_number + E.revision_number, + E.jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries E LEFT JOIN @@ -2062,7 +2066,8 @@ SELECT NULL AS trust_domain, NULL AS dns_name_id, NULL AS dns_name, - revision_number + revision_number, + jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries WHERE id IN (SELECT id FROM listing) @@ -2070,7 +2075,7 @@ WHERE id IN (SELECT id FROM listing) UNION SELECT - F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL + F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL, NULL FROM bundles B INNER JOIN @@ -2083,7 +2088,7 @@ WHERE UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL, NULL FROM dns_names WHERE registered_entry_id IN (SELECT id FROM listing) @@ -2091,7 +2096,7 @@ WHERE registered_entry_id IN (SELECT id FROM listing) UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL, NULL FROM selectors WHERE registered_entry_id IN (SELECT id FROM listing) @@ -2309,7 +2314,8 @@ SELECT NULL AS trust_domain, NULL AS dns_name_id, NULL AS dns_name, - revision_number + revision_number, + jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries `) @@ -2320,7 +2326,7 @@ FROM UNION SELECT - F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL + F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL, NULL FROM bundles B INNER JOIN @@ -2335,7 +2341,7 @@ ON UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL, NULL FROM dns_names `) @@ -2346,7 +2352,7 @@ FROM UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL, NULL FROM selectors `) @@ -2388,7 +2394,8 @@ SELECT NULL AS trust_domain, NULL ::integer AS dns_name_id, NULL AS dns_name, - revision_number + revision_number, + jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries `) @@ -2399,7 +2406,7 @@ FROM UNION SELECT - F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL + F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL, NULL FROM bundles B INNER JOIN @@ -2414,7 +2421,7 @@ ON UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL, NULL FROM dns_names `) @@ -2425,7 +2432,7 @@ FROM UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL, NULL FROM selectors `) @@ -2471,7 +2478,8 @@ SELECT B.trust_domain, D.id AS dns_name_id, D.value AS dns_name, - E.revision_number + E.revision_number, + E.jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries E LEFT JOIN @@ -2526,7 +2534,8 @@ SELECT NULL AS trust_domain, NULL AS dns_name_id, NULL AS dns_name, - revision_number + revision_number, + jwt_svid_ttl AS reg_jwt_svid_ttl FROM registered_entries `) @@ -2537,7 +2546,7 @@ FROM UNION SELECT - F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL + F.registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, B.trust_domain, NULL, NULL, NULL, NULL FROM bundles B INNER JOIN @@ -2552,7 +2561,7 @@ ON UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, value, NULL, NULL FROM dns_names `) @@ -2563,7 +2572,7 @@ FROM UNION SELECT - registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL + registered_entry_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, id, type, value, NULL, NULL, NULL, NULL, NULL FROM selectors `) @@ -3018,6 +3027,7 @@ type entryRow struct { DNSNameID sql.NullInt64 DNSName sql.NullString RevisionNumber sql.NullInt64 + RegJwtSvidTTL sql.NullInt64 } func scanEntryRow(rs *sql.Rows, r *entryRow) error { @@ -3038,6 +3048,7 @@ func scanEntryRow(rs *sql.Rows, r *entryRow) error { &r.DNSNameID, &r.DNSName, &r.RevisionNumber, + &r.RegJwtSvidTTL, )) } @@ -3051,9 +3062,6 @@ func fillEntryFromRow(entry *common.RegistrationEntry, r *entryRow) error { if r.ParentID.Valid { entry.ParentId = r.ParentID.String } - if r.RegTTL.Valid { - entry.Ttl = int32(r.RegTTL.Int64) - } if r.Admin.Valid { entry.Admin = r.Admin.Bool } @@ -3087,6 +3095,14 @@ func fillEntryFromRow(entry *common.RegistrationEntry, r *entryRow) error { if r.TrustDomain.Valid { entry.FederatesWith = append(entry.FederatesWith, r.TrustDomain.String) } + + if r.RegTTL.Valid { + entry.X509SvidTtl = int32(r.RegTTL.Int64) + } + + if r.RegJwtSvidTTL.Valid { + entry.JwtSvidTtl = int32(r.RegJwtSvidTTL.Int64) + } return nil } @@ -3166,8 +3182,8 @@ func updateRegistrationEntry(tx *gorm.DB, e *common.RegistrationEntry, mask *com if mask == nil || mask.ParentId { entry.ParentID = e.ParentId } - if mask == nil || mask.Ttl { - entry.TTL = e.Ttl + if mask == nil || mask.X509SvidTtl { + entry.TTL = e.X509SvidTtl } if mask == nil || mask.Admin { entry.Admin = e.Admin @@ -3178,6 +3194,9 @@ func updateRegistrationEntry(tx *gorm.DB, e *common.RegistrationEntry, mask *com if mask == nil || mask.EntryExpiry { entry.Expiry = e.EntryExpiry } + if mask == nil || mask.JwtSvidTtl { + entry.JWTSvidTTL = e.JwtSvidTtl + } // Revision number is increased by 1 on every update call entry.RevisionNumber++ @@ -3546,8 +3565,12 @@ func validateRegistrationEntry(entry *common.RegistrationEntry) error { return sqlError.New("invalid registration entry: missing SPIFFE ID") } - if entry.Ttl < 0 { - return sqlError.New("invalid registration entry: TTL is not set") + if entry.X509SvidTtl < 0 { + return sqlError.New("invalid registration entry: X509SvidTtl is not set") + } + + if entry.JwtSvidTtl < 0 { + return sqlError.New("invalid registration entry: JwtSvidTtl is not set") } return nil @@ -3582,9 +3605,14 @@ func validateRegistrationEntryForUpdate(entry *common.RegistrationEntry, mask *c return sqlError.New("invalid registration entry: missing SPIFFE ID") } - if (mask == nil || mask.Ttl) && - (entry.Ttl < 0) { - return sqlError.New("invalid registration entry: TTL is not set") + if (mask == nil || mask.X509SvidTtl) && + (entry.X509SvidTtl < 0) { + return sqlError.New("invalid registration entry: X509SvidTtl is not set") + } + + if (mask == nil || mask.JwtSvidTtl) && + (entry.JwtSvidTtl < 0) { + return sqlError.New("invalid registration entry: JwtSvidTtl is not set") } return nil @@ -3649,7 +3677,7 @@ func modelToEntry(tx *gorm.DB, model RegisteredEntry) (*common.RegistrationEntry Selectors: selectors, SpiffeId: model.SpiffeID, ParentId: model.ParentID, - Ttl: model.TTL, + X509SvidTtl: model.TTL, FederatesWith: federatesWith, Admin: model.Admin, Downstream: model.Downstream, @@ -3657,6 +3685,7 @@ func modelToEntry(tx *gorm.DB, model RegisteredEntry) (*common.RegistrationEntry DnsNames: dnsList, RevisionNumber: model.RevisionNumber, StoreSvid: model.StoreSvid, + JwtSvidTtl: model.JWTSvidTTL, }, nil } diff --git a/pkg/server/datastore/sqlstore/sqlstore_test.go b/pkg/server/datastore/sqlstore/sqlstore_test.go index ab1f2ff256..d6eb9fb782 100644 --- a/pkg/server/datastore/sqlstore/sqlstore_test.go +++ b/pkg/server/datastore/sqlstore/sqlstore_test.go @@ -1338,9 +1338,9 @@ func (s *PluginSuite) TestFetchRegistrationEntry() { {Type: "Type2", Value: "Value2"}, {Type: "Type3", Value: "Value3"}, }, - SpiffeId: "SpiffeId", - ParentId: "ParentId", - Ttl: 1, + SpiffeId: "SpiffeId", + ParentId: "ParentId", + X509SvidTtl: 1, DnsNames: []string{ "abcd.efg", "somehost", @@ -1353,10 +1353,10 @@ func (s *PluginSuite) TestFetchRegistrationEntry() { Selectors: []*common.Selector{ {Type: "Type1", Value: "Value1"}, }, - SpiffeId: "SpiffeId", - ParentId: "ParentId", - Ttl: 1, - StoreSvid: true, + SpiffeId: "SpiffeId", + ParentId: "ParentId", + X509SvidTtl: 1, + StoreSvid: true, }, }, } { @@ -1383,7 +1383,7 @@ func (s *PluginSuite) TestPruneRegistrationEntries() { }, SpiffeId: "SpiffeId", ParentId: "ParentId", - Ttl: 1, + X509SvidTtl: 1, EntryExpiry: now.Unix(), } @@ -2201,19 +2201,22 @@ func (s *PluginSuite) TestUpdateRegistrationEntry() { {Type: "Type2", Value: "Value2"}, {Type: "Type3", Value: "Value3"}, }, - SpiffeId: "spiffe://example.org/foo", - ParentId: "spiffe://example.org/bar", - Ttl: 1, + SpiffeId: "spiffe://example.org/foo", + ParentId: "spiffe://example.org/bar", + X509SvidTtl: 1, + JwtSvidTtl: 20, }) - entry.Ttl = 2 + entry.X509SvidTtl = 11 + entry.JwtSvidTtl = 21 entry.Admin = true entry.Downstream = true updatedRegistrationEntry, err := s.ds.UpdateRegistrationEntry(ctx, entry, nil) s.Require().NoError(err) // Verify output has expected values - s.Require().Equal(int32(2), entry.Ttl) + s.Require().Equal(int32(11), entry.X509SvidTtl) + s.Require().Equal(int32(21), entry.JwtSvidTtl) s.Require().True(entry.Admin) s.Require().True(entry.Downstream) @@ -2234,9 +2237,9 @@ func (s *PluginSuite) TestUpdateRegistrationEntryWithStoreSvid() { {Type: "Type1", Value: "Value2"}, {Type: "Type1", Value: "Value3"}, }, - SpiffeId: "spiffe://example.org/foo", - ParentId: "spiffe://example.org/bar", - Ttl: 1, + SpiffeId: "spiffe://example.org/foo", + ParentId: "spiffe://example.org/bar", + X509SvidTtl: 1, }) entry.StoreSvid = true @@ -2263,16 +2266,17 @@ func (s *PluginSuite) TestUpdateRegistrationEntryWithStoreSvid() { } func (s *PluginSuite) TestUpdateRegistrationEntryWithMask() { - // There are 9 fields in a registration entry. Of these, 3 have some validation in the SQL - // layer. In this test, we update each of the 9 fields and make sure update works, and also check - // with the mask value false to make sure nothing changes. For the 3 fields that have validation + // There are 11 fields in a registration entry. Of these, 5 have some validation in the SQL + // layer. In this test, we update each of the 11 fields and make sure update works, and also check + // with the mask value false to make sure nothing changes. For the 5 fields that have validation // we try with good data, bad data, and with or without a mask (so 4 cases each.) // Note that most of the input validation is done in the API layer and has more extensive tests there. oldEntry := &common.RegistrationEntry{ ParentId: "spiffe://example.org/oldParentId", SpiffeId: "spiffe://example.org/oldSpiffeId", - Ttl: 1000, + X509SvidTtl: 1000, + JwtSvidTtl: 3000, Selectors: []*common.Selector{{Type: "Type1", Value: "Value1"}}, FederatesWith: []string{"spiffe://dom1.org"}, Admin: false, @@ -2284,7 +2288,8 @@ func (s *PluginSuite) TestUpdateRegistrationEntryWithMask() { newEntry := &common.RegistrationEntry{ ParentId: "spiffe://example.org/oldParentId", SpiffeId: "spiffe://example.org/newSpiffeId", - Ttl: 1000, + X509SvidTtl: 4000, + JwtSvidTtl: 6000, Selectors: []*common.Selector{{Type: "Type2", Value: "Value2"}}, FederatesWith: []string{"spiffe://dom2.org"}, Admin: false, @@ -2296,7 +2301,8 @@ func (s *PluginSuite) TestUpdateRegistrationEntryWithMask() { badEntry := &common.RegistrationEntry{ ParentId: "not a good parent id", SpiffeId: "", - Ttl: -1000, + X509SvidTtl: -1000, + JwtSvidTtl: -3000, Selectors: []*common.Selector{}, FederatesWith: []string{"invalid federated bundle"}, Admin: false, @@ -2341,22 +2347,39 @@ func (s *PluginSuite) TestUpdateRegistrationEntryWithMask() { mask: &common.RegistrationEntryMask{ParentId: false}, update: func(e *common.RegistrationEntry) { e.ParentId = newEntry.ParentId }, result: func(e *common.RegistrationEntry) {}}, - // TTL FIELD -- This field is validated so we check with good and bad data - {name: "Update TTL, Good Data, Mask True", - mask: &common.RegistrationEntryMask{Ttl: true}, - update: func(e *common.RegistrationEntry) { e.Ttl = newEntry.Ttl }, - result: func(e *common.RegistrationEntry) { e.Ttl = newEntry.Ttl }}, - {name: "Update TTL, Good Data, Mask False", - mask: &common.RegistrationEntryMask{Ttl: false}, - update: func(e *common.RegistrationEntry) { e.Ttl = badEntry.Ttl }, + // X509 SVID TTL FIELD -- This field is validated so we check with good and bad data + {name: "Update X509 SVID TTL, Good Data, Mask True", + mask: &common.RegistrationEntryMask{X509SvidTtl: true}, + update: func(e *common.RegistrationEntry) { e.X509SvidTtl = newEntry.X509SvidTtl }, + result: func(e *common.RegistrationEntry) { e.X509SvidTtl = newEntry.X509SvidTtl }}, + {name: "Update X509 SVID TTL, Good Data, Mask False", + mask: &common.RegistrationEntryMask{X509SvidTtl: false}, + update: func(e *common.RegistrationEntry) { e.X509SvidTtl = badEntry.X509SvidTtl }, result: func(e *common.RegistrationEntry) {}}, - {name: "Update TTL, Bad Data, Mask True", - mask: &common.RegistrationEntryMask{Ttl: true}, - update: func(e *common.RegistrationEntry) { e.Ttl = badEntry.Ttl }, - err: errors.New("invalid registration entry: TTL is not set")}, - {name: "Update TTL, Bad Data, Mask False", - mask: &common.RegistrationEntryMask{Ttl: false}, - update: func(e *common.RegistrationEntry) { e.Ttl = badEntry.Ttl }, + {name: "Update X509 SVID TTL, Bad Data, Mask True", + mask: &common.RegistrationEntryMask{X509SvidTtl: true}, + update: func(e *common.RegistrationEntry) { e.X509SvidTtl = badEntry.X509SvidTtl }, + err: errors.New("invalid registration entry: X509SvidTtl is not set")}, + {name: "Update X509 SVID TTL, Bad Data, Mask False", + mask: &common.RegistrationEntryMask{X509SvidTtl: false}, + update: func(e *common.RegistrationEntry) { e.X509SvidTtl = badEntry.X509SvidTtl }, + result: func(e *common.RegistrationEntry) {}}, + // JWT SVID TTL FIELD -- This field is validated so we check with good and bad data + {name: "Update JWT SVID TTL, Good Data, Mask True", + mask: &common.RegistrationEntryMask{JwtSvidTtl: true}, + update: func(e *common.RegistrationEntry) { e.JwtSvidTtl = newEntry.JwtSvidTtl }, + result: func(e *common.RegistrationEntry) { e.JwtSvidTtl = newEntry.JwtSvidTtl }}, + {name: "Update JWT SVID TTL, Good Data, Mask False", + mask: &common.RegistrationEntryMask{JwtSvidTtl: false}, + update: func(e *common.RegistrationEntry) { e.JwtSvidTtl = badEntry.JwtSvidTtl }, + result: func(e *common.RegistrationEntry) {}}, + {name: "Update JWT SVID TTL, Bad Data, Mask True", + mask: &common.RegistrationEntryMask{JwtSvidTtl: true}, + update: func(e *common.RegistrationEntry) { e.JwtSvidTtl = badEntry.JwtSvidTtl }, + err: errors.New("invalid registration entry: JwtSvidTtl is not set")}, + {name: "Update JWT SVID TTL, Bad Data, Mask False", + mask: &common.RegistrationEntryMask{JwtSvidTtl: false}, + update: func(e *common.RegistrationEntry) { e.JwtSvidTtl = badEntry.JwtSvidTtl }, result: func(e *common.RegistrationEntry) {}}, // SELECTORS FIELD -- This field is validated so we check with good and bad data {name: "Update Selectors, Good Data, Mask True", @@ -2494,9 +2517,9 @@ func (s *PluginSuite) TestDeleteRegistrationEntry() { {Type: "Type2", Value: "Value2"}, {Type: "Type3", Value: "Value3"}, }, - SpiffeId: "spiffe://example.org/foo", - ParentId: "spiffe://example.org/bar", - Ttl: 1, + SpiffeId: "spiffe://example.org/foo", + ParentId: "spiffe://example.org/bar", + X509SvidTtl: 1, }) s.createRegistrationEntry(&common.RegistrationEntry{ @@ -2505,9 +2528,9 @@ func (s *PluginSuite) TestDeleteRegistrationEntry() { {Type: "Type4", Value: "Value4"}, {Type: "Type5", Value: "Value5"}, }, - SpiffeId: "spiffe://example.org/baz", - ParentId: "spiffe://example.org/bat", - Ttl: 2, + SpiffeId: "spiffe://example.org/baz", + ParentId: "spiffe://example.org/bat", + X509SvidTtl: 2, }) // We have two registration entries diff --git a/pkg/server/datastore/sqlstore/testdata/invalid_registration_entries.json b/pkg/server/datastore/sqlstore/testdata/invalid_registration_entries.json index 0b40c0fa5e..0628f709c3 100644 --- a/pkg/server/datastore/sqlstore/testdata/invalid_registration_entries.json +++ b/pkg/server/datastore/sqlstore/testdata/invalid_registration_entries.json @@ -15,7 +15,7 @@ } ], "spiffe_id": "SpiffeId", - "ttl": -5 + "x509_svid_ttl": -5 }, { "spiffe_id": "SpiffeId" @@ -32,7 +32,7 @@ } ], "spiffe_id": "SpiffeId3", - "ttl": 2, + "x509_svid_ttl": 2, "store_svid": true }, null diff --git a/pkg/server/plugin/keymanager/awskms/awskms.go b/pkg/server/plugin/keymanager/awskms/awskms.go index d31b81ee61..612d82d83c 100644 --- a/pkg/server/plugin/keymanager/awskms/awskms.go +++ b/pkg/server/plugin/keymanager/awskms/awskms.go @@ -23,6 +23,7 @@ import ( keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire/pkg/common/catalog" + "github.com/spiffe/spire/pkg/common/diskutil" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -947,7 +948,7 @@ func createServerID(idPath string) (string, error) { id := u.String() // persist id - err = os.WriteFile(idPath, []byte(id), 0600) + err = diskutil.WritePrivateFile(idPath, []byte(id)) if err != nil { return "", status.Errorf(codes.Internal, "failed to persist server id on path: %v", err) } diff --git a/pkg/server/plugin/keymanager/base/keymanagerbase.go b/pkg/server/plugin/keymanager/base/keymanagerbase.go index befc16d42b..9d9c521906 100644 --- a/pkg/server/plugin/keymanager/base/keymanagerbase.go +++ b/pkg/server/plugin/keymanager/base/keymanagerbase.go @@ -26,42 +26,40 @@ type KeyEntry struct { *keymanagerv1.PublicKey } -// Funcs is a collection of optional callbacks. Default implementations will be +// Config is a collection of optional callbacks. Default implementations will be // used when not provided. -type Funcs struct { - WriteEntries func(ctx context.Context, entries []*KeyEntry) error - GenerateRSA2048Key func() (*rsa.PrivateKey, error) - GenerateRSA4096Key func() (*rsa.PrivateKey, error) - GenerateEC256Key func() (*ecdsa.PrivateKey, error) - GenerateEC384Key func() (*ecdsa.PrivateKey, error) +type Config struct { + // Generator is an optional key generator. + Generator Generator + + // WriteEntries is an optional callback used to persist key entries + WriteEntries func(ctx context.Context, entries []*KeyEntry) error +} + +// Generator is a key generator +type Generator interface { + GenerateRSA2048Key() (*rsa.PrivateKey, error) + GenerateRSA4096Key() (*rsa.PrivateKey, error) + GenerateEC256Key() (*ecdsa.PrivateKey, error) + GenerateEC384Key() (*ecdsa.PrivateKey, error) } // Base is the base KeyManager implementation type Base struct { keymanagerv1.UnsafeKeyManagerServer - funcs Funcs + config Config mu sync.RWMutex entries map[string]*KeyEntry } -// New creates a new base key manager using the provided Funcs. Default -// implementations are provided for any that aren't set. -func New(funcs Funcs) *Base { - if funcs.GenerateRSA2048Key == nil { - funcs.GenerateRSA2048Key = generateRSA2048Key - } - if funcs.GenerateRSA4096Key == nil { - funcs.GenerateRSA4096Key = generateRSA4096Key - } - if funcs.GenerateEC256Key == nil { - funcs.GenerateEC256Key = generateEC256Key - } - if funcs.GenerateEC384Key == nil { - funcs.GenerateEC384Key = generateEC384Key +// New creates a new base key manager using the provided config. +func New(config Config) *Base { + if config.Generator == nil { + config.Generator = defaultGenerator{} } return &Base{ - funcs: funcs, + config: config, entries: make(map[string]*KeyEntry), } } @@ -142,8 +140,8 @@ func (m *Base) generateKey(ctx context.Context, req *keymanagerv1.GenerateKeyReq m.entries[req.KeyId] = newEntry - if m.funcs.WriteEntries != nil { - if err := m.funcs.WriteEntries(ctx, entriesSliceFromMap(m.entries)); err != nil { + if m.config.WriteEntries != nil { + if err := m.config.WriteEntries(ctx, entriesSliceFromMap(m.entries)); err != nil { if hasEntry { m.entries[req.KeyId] = oldEntry } else { @@ -217,13 +215,13 @@ func (m *Base) generateKeyEntry(keyID string, keyType keymanagerv1.KeyType) (e * var privateKey crypto.Signer switch keyType { case keymanagerv1.KeyType_EC_P256: - privateKey, err = m.funcs.GenerateEC256Key() + privateKey, err = m.config.Generator.GenerateEC256Key() case keymanagerv1.KeyType_EC_P384: - privateKey, err = m.funcs.GenerateEC384Key() + privateKey, err = m.config.Generator.GenerateEC384Key() case keymanagerv1.KeyType_RSA_2048: - privateKey, err = m.funcs.GenerateRSA2048Key() + privateKey, err = m.config.Generator.GenerateRSA2048Key() case keymanagerv1.KeyType_RSA_4096: - privateKey, err = m.funcs.GenerateRSA4096Key() + privateKey, err = m.config.Generator.GenerateRSA4096Key() default: return nil, status.Errorf(codes.InvalidArgument, "unable to generate key %q for unknown key type %q", keyID, keyType) } @@ -299,19 +297,21 @@ func ecdsaKeyType(privateKey *ecdsa.PrivateKey) (keymanagerv1.KeyType, error) { } } -func generateRSA2048Key() (*rsa.PrivateKey, error) { +type defaultGenerator struct{} + +func (defaultGenerator) GenerateRSA2048Key() (*rsa.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, 2048) } -func generateRSA4096Key() (*rsa.PrivateKey, error) { +func (defaultGenerator) GenerateRSA4096Key() (*rsa.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, 4096) } -func generateEC256Key() (*ecdsa.PrivateKey, error) { +func (defaultGenerator) GenerateEC256Key() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) } -func generateEC384Key() (*ecdsa.PrivateKey, error) { +func (defaultGenerator) GenerateEC384Key() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) } diff --git a/pkg/server/plugin/keymanager/base/keymanagerbase_test.go b/pkg/server/plugin/keymanager/base/keymanagerbase_test.go new file mode 100644 index 0000000000..623717f3f1 --- /dev/null +++ b/pkg/server/plugin/keymanager/base/keymanagerbase_test.go @@ -0,0 +1,14 @@ +package keymanagerbase + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSetsConfigDefaults(t *testing.T) { + // This test makes sure that we wire up the default functions + b := New(Config{}) + assert.Equal(t, defaultGenerator{}, b.config.Generator) + assert.Nil(t, b.config.WriteEntries) +} diff --git a/pkg/server/plugin/keymanager/disk/disk.go b/pkg/server/plugin/keymanager/disk/disk.go index e702efef08..0dc6b01b8d 100644 --- a/pkg/server/plugin/keymanager/disk/disk.go +++ b/pkg/server/plugin/keymanager/disk/disk.go @@ -17,11 +17,17 @@ import ( "google.golang.org/grpc/status" ) +type Generator = keymanagerbase.Generator + func BuiltIn() catalog.BuiltIn { - return builtin(New()) + return asBuiltIn(newKeyManager(nil)) +} + +func TestBuiltIn(generator Generator) catalog.BuiltIn { + return asBuiltIn(newKeyManager(generator)) } -func builtin(p *KeyManager) catalog.BuiltIn { +func asBuiltIn(p *KeyManager) catalog.BuiltIn { return catalog.MakeBuiltIn("disk", keymanagerv1.KeyManagerPluginServer(p), configv1.ConfigServiceServer(p)) @@ -39,10 +45,11 @@ type KeyManager struct { config *configuration } -func New() *KeyManager { +func newKeyManager(generator Generator) *KeyManager { m := &KeyManager{} - m.Base = keymanagerbase.New(keymanagerbase.Funcs{ + m.Base = keymanagerbase.New(keymanagerbase.Config{ WriteEntries: m.writeEntries, + Generator: generator, }) return m } @@ -143,7 +150,7 @@ func writeEntries(path string, entries []*keymanagerbase.KeyEntry) error { return status.Errorf(codes.Internal, "unable to marshal entries: %v", err) } - if err := diskutil.AtomicWriteFile(path, jsonBytes, 0600); err != nil { + if err := diskutil.AtomicWritePrivateFile(path, jsonBytes); err != nil { return status.Errorf(codes.Internal, "unable to write entries: %v", err) } diff --git a/pkg/server/plugin/keymanager/disk/disk_test.go b/pkg/server/plugin/keymanager/disk/disk_test.go index cc050c4093..fcaf04637b 100644 --- a/pkg/server/plugin/keymanager/disk/disk_test.go +++ b/pkg/server/plugin/keymanager/disk/disk_test.go @@ -83,7 +83,7 @@ func TestGenerateKeyPersistence(t *testing.T) { func loadPlugin(t *testing.T, configFmt string, configArgs ...interface{}) (keymanager.KeyManager, error) { km := new(keymanager.V1) var configErr error - plugintest.Load(t, disk.BuiltIn(), km, + plugintest.Load(t, disk.TestBuiltIn(keymanagertest.NewGenerator()), km, plugintest.Configuref(configFmt, configArgs...), plugintest.CaptureConfigureError(&configErr), ) diff --git a/pkg/server/plugin/keymanager/gcpkms/client.go b/pkg/server/plugin/keymanager/gcpkms/client.go new file mode 100644 index 0000000000..704ae78400 --- /dev/null +++ b/pkg/server/plugin/keymanager/gcpkms/client.go @@ -0,0 +1,129 @@ +package gcpkms + +import ( + "context" + + "cloud.google.com/go/iam" + "cloud.google.com/go/iam/apiv1/iampb" + kms "cloud.google.com/go/kms/apiv1" + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/googleapis/gax-go/v2" + "google.golang.org/api/oauth2/v2" + "google.golang.org/api/option" +) + +type cloudKeyManagementService interface { + AsymmetricSign(context.Context, *kmspb.AsymmetricSignRequest, ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) + Close() error + CreateCryptoKey(context.Context, *kmspb.CreateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error) + CreateCryptoKeyVersion(context.Context, *kmspb.CreateCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) + DestroyCryptoKeyVersion(context.Context, *kmspb.DestroyCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) + GetCryptoKeyVersion(context.Context, *kmspb.GetCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) + GetPublicKey(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error) + GetTokeninfo() (*oauth2.Tokeninfo, error) + ListCryptoKeys(context.Context, *kmspb.ListCryptoKeysRequest, ...gax.CallOption) cryptoKeyIterator + ListCryptoKeyVersions(context.Context, *kmspb.ListCryptoKeyVersionsRequest, ...gax.CallOption) cryptoKeyVersionIterator + ResourceIAM(string) iamHandler + UpdateCryptoKey(context.Context, *kmspb.UpdateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error) +} + +type kmsClient struct { + client *kms.KeyManagementClient + oauth2Service *oauth2.Service +} + +func (c *kmsClient) AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) { + return c.client.AsymmetricSign(ctx, req, opts...) +} + +func (c *kmsClient) Close() error { + return c.client.Close() +} + +func (c *kmsClient) CreateCryptoKey(ctx context.Context, req *kmspb.CreateCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) { + return c.client.CreateCryptoKey(ctx, req, opts...) +} + +func (c *kmsClient) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return c.client.CreateCryptoKeyVersion(ctx, req, opts...) +} + +func (c *kmsClient) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.DestroyCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return c.client.DestroyCryptoKeyVersion(ctx, req, opts...) +} + +func (c *kmsClient) GetCryptoKeyVersion(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return c.client.GetCryptoKeyVersion(ctx, req, opts...) +} + +func (c *kmsClient) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) { + return c.client.GetPublicKey(ctx, req, opts...) +} + +func (c *kmsClient) GetTokeninfo() (*oauth2.Tokeninfo, error) { + return c.oauth2Service.Tokeninfo().Do() +} + +func (c *kmsClient) ListCryptoKeys(ctx context.Context, req *kmspb.ListCryptoKeysRequest, opts ...gax.CallOption) cryptoKeyIterator { + return c.client.ListCryptoKeys(ctx, req, opts...) +} + +func (c *kmsClient) ListCryptoKeyVersions(ctx context.Context, req *kmspb.ListCryptoKeyVersionsRequest, opts ...gax.CallOption) cryptoKeyVersionIterator { + return c.client.ListCryptoKeyVersions(ctx, req, opts...) +} + +func (c *kmsClient) ResourceIAM(resourcePath string) iamHandler { + return &iamHandle{ + h: c.client.ResourceIAM(resourcePath), + } +} + +func (c *kmsClient) SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) { + return c.client.SetIamPolicy(ctx, req, opts...) +} + +func (c *kmsClient) UpdateCryptoKey(ctx context.Context, req *kmspb.UpdateCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) { + return c.client.UpdateCryptoKey(ctx, req, opts...) +} + +type cryptoKeyIterator interface { + Next() (*kmspb.CryptoKey, error) +} + +type cryptoKeyVersionIterator interface { + Next() (*kmspb.CryptoKeyVersion, error) +} + +type iamHandler interface { + V3() iamHandler3 +} + +type iamHandler3 interface { + Policy(context.Context) (*iam.Policy3, error) + SetPolicy(context.Context, *iam.Policy3) error +} + +type iamHandle struct { + h *iam.Handle +} + +func (i *iamHandle) V3() iamHandler3 { + return i.h.V3() +} + +func newKMSClient(ctx context.Context, opts ...option.ClientOption) (cloudKeyManagementService, error) { + client, err := kms.NewKeyManagementClient(ctx, opts...) + if err != nil { + return nil, err + } + + oauth2Service, err := oauth2.NewService(ctx, opts...) + if err != nil { + return nil, err + } + + return &kmsClient{ + client: client, + oauth2Service: oauth2Service, + }, nil +} diff --git a/pkg/server/plugin/keymanager/gcpkms/client_fake.go b/pkg/server/plugin/keymanager/gcpkms/client_fake.go new file mode 100644 index 0000000000..7499004562 --- /dev/null +++ b/pkg/server/plugin/keymanager/gcpkms/client_fake.go @@ -0,0 +1,775 @@ +package gcpkms + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "path" + "reflect" + "strings" + "sync" + "testing" + + "cloud.google.com/go/iam" + "cloud.google.com/go/iam/apiv1/iampb" + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/googleapis/gax-go/v2" + "github.com/spiffe/spire/test/clock" + "github.com/spiffe/spire/test/testkey" + "google.golang.org/api/iterator" + "google.golang.org/api/oauth2/v2" + "google.golang.org/api/option" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +type fakeCryptoKeyIterator struct { + mu sync.RWMutex + + index int + cryptoKeys []*kmspb.CryptoKey + nextErr error +} + +func (i *fakeCryptoKeyIterator) Next() (cryptoKey *kmspb.CryptoKey, err error) { + i.mu.Lock() + defer i.mu.Unlock() + + if i.nextErr != nil { + return nil, i.nextErr + } + + if i.index >= len(i.cryptoKeys) { + return nil, iterator.Done + } + + cryptoKey = i.cryptoKeys[i.index] + i.index++ + return cryptoKey, nil +} + +type fakeCryptoKeyVersionIterator struct { + mu sync.RWMutex + + index int + cryptoKeyVersions []*kmspb.CryptoKeyVersion + nextErr error +} + +func (i *fakeCryptoKeyVersionIterator) Next() (cryptoKeyVersion *kmspb.CryptoKeyVersion, err error) { + i.mu.Lock() + defer i.mu.Unlock() + + if i.nextErr != nil { + return nil, i.nextErr + } + + if i.index >= len(i.cryptoKeyVersions) { + return nil, iterator.Done + } + + cryptoKeyVersion = i.cryptoKeyVersions[i.index] + i.index++ + return cryptoKeyVersion, nil +} + +type fakeCryptoKey struct { + mu sync.RWMutex + *kmspb.CryptoKey + fakeCryptoKeyVersions map[string]*fakeCryptoKeyVersion +} + +func (fck *fakeCryptoKey) fetchFakeCryptoKeyVersions() map[string]*fakeCryptoKeyVersion { + fck.mu.RLock() + defer fck.mu.RUnlock() + + if fck.fakeCryptoKeyVersions == nil { + return nil + } + + fakeCryptoKeyVersions := make(map[string]*fakeCryptoKeyVersion, len(fck.fakeCryptoKeyVersions)) + for key, fakeCryptoKeyVersion := range fck.fakeCryptoKeyVersions { + fakeCryptoKeyVersions[key] = fakeCryptoKeyVersion + } + return fakeCryptoKeyVersions +} + +func (fck *fakeCryptoKey) getLabelValue(key string) string { + fck.mu.RLock() + defer fck.mu.RUnlock() + + return fck.Labels[key] +} + +func (fck *fakeCryptoKey) getName() string { + fck.mu.RLock() + defer fck.mu.RUnlock() + + return fck.Name +} + +func (fck *fakeCryptoKey) putFakeCryptoKeyVersion(fckv *fakeCryptoKeyVersion) { + fck.mu.Lock() + defer fck.mu.Unlock() + + fck.fakeCryptoKeyVersions[path.Base(fckv.Name)] = fckv +} + +type fakeCryptoKeyVersion struct { + *kmspb.CryptoKeyVersion + + privateKey crypto.Signer + publicKey *kmspb.PublicKey +} + +type fakeStore struct { + mu sync.RWMutex + fakeCryptoKeys map[string]*fakeCryptoKey + + clk *clock.Mock +} + +func (fs *fakeStore) fetchFakeCryptoKey(name string) (*fakeCryptoKey, bool) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + fakeCryptoKey, ok := fs.fakeCryptoKeys[name] + return fakeCryptoKey, ok +} + +func (fs *fakeStore) fetchFakeCryptoKeys() map[string]*fakeCryptoKey { + fs.mu.RLock() + defer fs.mu.RUnlock() + + if fs.fakeCryptoKeys == nil { + return nil + } + + fakeCryptoKeys := make(map[string]*fakeCryptoKey, len(fs.fakeCryptoKeys)) + for key, fakeCryptoKey := range fs.fakeCryptoKeys { + fakeCryptoKeys[key] = fakeCryptoKey + } + return fakeCryptoKeys +} + +func (fs *fakeStore) fetchFakeCryptoKeyVersion(name string) (fakeCryptoKeyVersion, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + parent := path.Dir(path.Dir(name)) + fakeCryptoKey, ok := fs.fakeCryptoKeys[parent] + if !ok { + return fakeCryptoKeyVersion{}, fmt.Errorf("could not get parent CryptoKey for %q CryptoKeyVersion", name) + } + + version := path.Base(name) + fakeCryptoKey.mu.RLock() + defer fakeCryptoKey.mu.RUnlock() + fakeCryptokeyVersion, ok := fakeCryptoKey.fakeCryptoKeyVersions[version] + if ok { + return *fakeCryptokeyVersion, nil + } + + return fakeCryptoKeyVersion{}, fmt.Errorf("could not find CryptoKeyVersion %q", version) +} + +func (fs *fakeStore) putFakeCryptoKey(fck *fakeCryptoKey) { + fs.mu.Lock() + defer fs.mu.Unlock() + + fs.fakeCryptoKeys[fck.Name] = fck +} + +type fakeIAMHandle struct { + mu sync.RWMutex + expectedPolicy *iam.Policy3 + policyErr error + setPolicyErr error +} + +func (h *fakeIAMHandle) V3() iamHandler3 { + h.mu.RLock() + defer h.mu.RUnlock() + + return &fakeIAMHandle3{ + expectedPolicy: h.expectedPolicy, + policyErr: h.policyErr, + setPolicyErr: h.setPolicyErr, + } +} + +func (h *fakeIAMHandle) setExpectedPolicy(expectedPolicy *iam.Policy3) { + h.mu.Lock() + defer h.mu.Unlock() + + h.expectedPolicy = expectedPolicy +} + +func (h *fakeIAMHandle) setPolicyError(fakeError error) { + h.mu.Lock() + defer h.mu.Unlock() + + h.policyErr = fakeError +} + +func (h *fakeIAMHandle) setSetPolicyErr(fakeError error) { + h.mu.Lock() + defer h.mu.Unlock() + + h.setPolicyErr = fakeError +} + +type fakeIAMHandle3 struct { + mu sync.RWMutex + expectedPolicy *iam.Policy3 + policyErr error + setPolicyErr error +} + +func (h3 *fakeIAMHandle3) Policy(context.Context) (*iam.Policy3, error) { + h3.mu.RLock() + defer h3.mu.RUnlock() + + if h3.policyErr != nil { + return nil, h3.policyErr + } + return &iam.Policy3{}, nil +} + +func (h3 *fakeIAMHandle3) SetPolicy(ctx context.Context, policy *iam.Policy3) error { + h3.mu.Lock() + defer h3.mu.Unlock() + + if h3.expectedPolicy != nil { + if !reflect.DeepEqual(h3.expectedPolicy, policy) { + return fmt.Errorf("unexpected policy: %v", policy) + } + } + + return h3.setPolicyErr +} + +type fakeKMSClient struct { + t *testing.T + + mu sync.RWMutex + asymmetricSignErr error + closeErr error + createCryptoKeyErr error + initialCryptoKeyVersionState kmspb.CryptoKeyVersion_CryptoKeyVersionState + destroyCryptoKeyVersionErr error + destroyTime *timestamppb.Timestamp + fakeIAMHandle *fakeIAMHandle + getCryptoKeyVersionErr error + getPublicKeyErrs []error + getTokeninfoErr error + listCryptoKeysErr error + listCryptoKeyVersionsErr error + opts []option.ClientOption + pemCrc32C *wrapperspb.Int64Value + signatureCrc32C *wrapperspb.Int64Value + store fakeStore + tokeninfo *oauth2.Tokeninfo + updateCryptoKeyErr error + keyIsDisabled bool +} + +func (k *fakeKMSClient) setAsymmetricSignErr(fakeError error) { + k.mu.Lock() + defer k.mu.Unlock() + + k.asymmetricSignErr = fakeError +} + +func (k *fakeKMSClient) setCreateCryptoKeyErr(fakeError error) { + k.mu.Lock() + defer k.mu.Unlock() + + k.createCryptoKeyErr = fakeError +} + +func (k *fakeKMSClient) setInitialCryptoKeyVersionState(state kmspb.CryptoKeyVersion_CryptoKeyVersionState) { + k.initialCryptoKeyVersionState = state +} + +func (k *fakeKMSClient) setDestroyCryptoKeyVersionErr(fakeError error) { + k.mu.Lock() + defer k.mu.Unlock() + + k.destroyCryptoKeyVersionErr = fakeError +} + +func (k *fakeKMSClient) setDestroyTime(fakeDestroyTime *timestamppb.Timestamp) { + k.mu.Lock() + defer k.mu.Unlock() + + k.destroyTime = fakeDestroyTime +} + +func (k *fakeKMSClient) setGetCryptoKeyVersionErr(fakeError error) { + k.mu.Lock() + defer k.mu.Unlock() + + k.getCryptoKeyVersionErr = fakeError +} + +func (k *fakeKMSClient) setIsKeyDisabled(ok bool) { + k.mu.Lock() + defer k.mu.Unlock() + + k.keyIsDisabled = ok +} + +func (k *fakeKMSClient) setGetPublicKeySequentialErrs(fakeError error, count int) { + k.mu.Lock() + defer k.mu.Unlock() + fakeErrors := make([]error, count) + for i := 0; i < count; i++ { + fakeErrors[i] = fakeError + } + k.getPublicKeyErrs = fakeErrors +} + +func (k *fakeKMSClient) nextGetPublicKeySequentialErr() error { + k.mu.Lock() + defer k.mu.Unlock() + if len(k.getPublicKeyErrs) == 0 { + return nil + } + err := k.getPublicKeyErrs[0] + k.getPublicKeyErrs = k.getPublicKeyErrs[1:] + return err +} + +func (k *fakeKMSClient) setGetTokeninfoErr(fakeError error) { + k.mu.Lock() + defer k.mu.Unlock() + + k.getTokeninfoErr = fakeError +} + +func (k *fakeKMSClient) setListCryptoKeysErr(fakeError error) { + k.mu.Lock() + defer k.mu.Unlock() + + k.listCryptoKeysErr = fakeError +} + +func (k *fakeKMSClient) setPEMCrc32C(pemCrc32C *wrapperspb.Int64Value) { + k.mu.Lock() + defer k.mu.Unlock() + + k.pemCrc32C = pemCrc32C +} + +func (k *fakeKMSClient) setSignatureCrc32C(signatureCrc32C *wrapperspb.Int64Value) { + k.mu.Lock() + defer k.mu.Unlock() + + k.signatureCrc32C = signatureCrc32C +} + +func (k *fakeKMSClient) setUpdateCryptoKeyErr(fakeError error) { + k.mu.Lock() + defer k.mu.Unlock() + + k.updateCryptoKeyErr = fakeError +} + +func (k *fakeKMSClient) AsymmetricSign(ctx context.Context, signReq *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + if k.asymmetricSignErr != nil { + return nil, k.asymmetricSignErr + } + + if signReq.Digest == nil { + return nil, status.Error(codes.InvalidArgument, "plugin should be signing over a digest") + } + + fakeCryptoKeyVersion, err := k.store.fetchFakeCryptoKeyVersion(signReq.Name) + if err != nil { + return nil, err + } + + signRSA := func(digest []byte, opts crypto.SignerOpts) ([]byte, error) { + if _, ok := fakeCryptoKeyVersion.privateKey.(*rsa.PrivateKey); !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid signing algorithm for RSA key") + } + return fakeCryptoKeyVersion.privateKey.Sign(rand.Reader, digest, opts) + } + signECDSA := func(digest []byte, opts crypto.SignerOpts) ([]byte, error) { + if _, ok := fakeCryptoKeyVersion.privateKey.(*ecdsa.PrivateKey); !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid signing algorithm for ECDSA key") + } + return fakeCryptoKeyVersion.privateKey.Sign(rand.Reader, digest, opts) + } + + cryptoKeyName := path.Dir(path.Dir(signReq.Name)) + fck, ok := k.store.fetchFakeCryptoKey(cryptoKeyName) + if !ok { + return nil, status.Errorf(codes.Internal, "could not find CryptoKey %q", cryptoKeyName) + } + var signature []byte + switch fck.VersionTemplate.Algorithm { + case kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: + signature, err = signECDSA(signReq.Digest.GetSha256(), crypto.SHA256) + case kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384: + signature, err = signECDSA(signReq.Digest.GetSha384(), crypto.SHA384) + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256: + signature, err = signRSA(signReq.Digest.GetSha256(), crypto.SHA256) + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256: + signature, err = signRSA(signReq.Digest.GetSha256(), crypto.SHA256) + default: + return nil, status.Errorf(codes.InvalidArgument, "unsupported signing algorithm: %s", fck.VersionTemplate.Algorithm) + } + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to sign digest: %v", err) + } + + signatureCrc32C := &wrapperspb.Int64Value{Value: int64(crc32Checksum(signature))} + if k.signatureCrc32C != nil { + // Override the SignatureCrc32C value + signatureCrc32C = k.signatureCrc32C + } + + return &kmspb.AsymmetricSignResponse{ + Signature: signature, + SignatureCrc32C: signatureCrc32C, + Name: signReq.Name, + }, nil +} + +func (k *fakeKMSClient) Close() error { + k.mu.RLock() + defer k.mu.RUnlock() + + return k.closeErr +} + +func (k *fakeKMSClient) CreateCryptoKey(ctx context.Context, req *kmspb.CreateCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + if k.createCryptoKeyErr != nil { + return nil, k.createCryptoKeyErr + } + + cryptoKey := &kmspb.CryptoKey{ + Name: path.Join(req.Parent, req.CryptoKeyId), + Labels: req.CryptoKey.Labels, + VersionTemplate: req.CryptoKey.VersionTemplate, + } + version := "1" + fckv, err := k.createFakeCryptoKeyVersion(cryptoKey, version) + if err != nil { + return nil, err + } + + fck := &fakeCryptoKey{ + CryptoKey: cryptoKey, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + version: fckv, + }, + } + k.store.putFakeCryptoKey(fck) + + return cryptoKey, nil +} + +func (k *fakeKMSClient) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + k.mu.Lock() + defer k.mu.Unlock() + + if k.createCryptoKeyErr != nil { + return nil, k.createCryptoKeyErr + } + + fck, ok := k.store.fakeCryptoKeys[req.Parent] + if !ok { + return nil, fmt.Errorf("could not find parent CryptoKey %q", req.Parent) + } + fckv, err := k.createFakeCryptoKeyVersion(fck.CryptoKey, fmt.Sprint(len(fck.fakeCryptoKeyVersions)+1)) + if err != nil { + return nil, err + } + + fck.putFakeCryptoKeyVersion(fckv) + + return &kmspb.CryptoKeyVersion{ + Algorithm: req.CryptoKeyVersion.Algorithm, + Name: fckv.Name, + State: kmspb.CryptoKeyVersion_ENABLED, + }, nil +} + +func (k *fakeKMSClient) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.DestroyCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + if k.destroyCryptoKeyVersionErr != nil { + return nil, k.destroyCryptoKeyVersionErr + } + + parent := path.Dir(path.Dir(req.Name)) + fck, ok := k.store.fetchFakeCryptoKey(parent) + if !ok { + return nil, fmt.Errorf("could not get parent CryptoKey for %q CryptoKeyVersion", parent) + } + + fckv, err := k.store.fetchFakeCryptoKeyVersion(req.Name) + if err != nil { + return nil, err + } + + var destroyTime *timestamppb.Timestamp + if k.destroyTime != nil { + destroyTime = k.destroyTime + } else { + destroyTime = timestamppb.Now() + } + + cryptoKeyVersion := &kmspb.CryptoKeyVersion{ + DestroyTime: destroyTime, + Name: fckv.Name, + State: kmspb.CryptoKeyVersion_DESTROY_SCHEDULED, + } + + fckv.CryptoKeyVersion = cryptoKeyVersion + fck.putFakeCryptoKeyVersion(&fckv) + + return cryptoKeyVersion, nil +} + +func (k *fakeKMSClient) GetCryptoKeyVersion(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + if k.getCryptoKeyVersionErr != nil { + return nil, k.getCryptoKeyVersionErr + } + + fakeCryptoKeyVersion, err := k.store.fetchFakeCryptoKeyVersion(req.Name) + if err != nil { + return nil, err + } + + if k.keyIsDisabled { + fakeCryptoKeyVersion.CryptoKeyVersion.State = kmspb.CryptoKeyVersion_DISABLED + } + return fakeCryptoKeyVersion.CryptoKeyVersion, nil +} + +func (k *fakeKMSClient) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) { + getPublicKeyErr := k.nextGetPublicKeySequentialErr() + + if getPublicKeyErr != nil { + return nil, getPublicKeyErr + } + + fakeCryptoKeyVersion, err := k.store.fetchFakeCryptoKeyVersion(req.Name) + if err != nil { + return nil, err + } + + if k.pemCrc32C != nil { + // Override pemCrc32C + fakeCryptoKeyVersion.publicKey.PemCrc32C = k.pemCrc32C + } + + return fakeCryptoKeyVersion.publicKey, nil +} + +func (k *fakeKMSClient) GetTokeninfo() (*oauth2.Tokeninfo, error) { + k.mu.RLock() + defer k.mu.RUnlock() + + return k.tokeninfo, k.getTokeninfoErr +} + +func (k *fakeKMSClient) ListCryptoKeys(ctx context.Context, req *kmspb.ListCryptoKeysRequest, opts ...gax.CallOption) cryptoKeyIterator { + k.mu.RLock() + defer k.mu.RUnlock() + + if k.listCryptoKeysErr != nil { + return &fakeCryptoKeyIterator{nextErr: k.listCryptoKeysErr} + } + var cryptoKeys []*kmspb.CryptoKey + fakeCryptoKeys := k.store.fetchFakeCryptoKeys() + + for _, fck := range fakeCryptoKeys { + // Make sure that it's within the same Key Ring. + // The Key Ring name es specified in req.Parent. + // The Key Ring name is three levels up from the CryptoKey name. + if req.Parent != path.Dir(path.Dir(path.Dir(fck.Name))) { + // Key Ring doesn't match. + continue + } + + // We Have a simplified filtering logic in this fake implementation, + // where we only care about the spire-active label. + if req.Filter != "" { + if !strings.Contains(req.Filter, "labels.spire-active = true") { + { + k.t.Fatal("Unsupported filter in ListCryptoKeys request") + } + if fck.Labels[labelNameActive] != "true" { + continue + } + } + } + + cryptoKeys = append(cryptoKeys, fck.CryptoKey) + } + + return &fakeCryptoKeyIterator{cryptoKeys: cryptoKeys} +} + +func (k *fakeKMSClient) ListCryptoKeyVersions(ctx context.Context, req *kmspb.ListCryptoKeyVersionsRequest, opts ...gax.CallOption) cryptoKeyVersionIterator { + k.mu.RLock() + defer k.mu.RUnlock() + + if k.listCryptoKeyVersionsErr != nil { + return &fakeCryptoKeyVersionIterator{nextErr: k.listCryptoKeyVersionsErr} + } + + var cryptoKeyVersions []*kmspb.CryptoKeyVersion + fck, ok := k.store.fakeCryptoKeys[req.Parent] + if !ok { + return &fakeCryptoKeyVersionIterator{nextErr: errors.New("parent CryptoKey not found")} + } + + for _, fckv := range fck.fakeCryptoKeyVersions { + // We Have a simplified filtering logic in this fake implementation, + // where we only support filtering by enabled status. + if req.Filter != "" { + if req.Filter != "state = "+kmspb.CryptoKeyVersion_ENABLED.String() { + k.t.Fatal("Unsupported filter in ListCryptoKeyVersions request") + } + if fckv.State != kmspb.CryptoKeyVersion_ENABLED { + continue + } + } + cryptoKeyVersions = append(cryptoKeyVersions, fckv.CryptoKeyVersion) + } + + return &fakeCryptoKeyVersionIterator{cryptoKeyVersions: cryptoKeyVersions} +} + +func (k *fakeKMSClient) ResourceIAM(string) iamHandler { + k.mu.RLock() + defer k.mu.RUnlock() + + return k.fakeIAMHandle +} + +func (k *fakeKMSClient) UpdateCryptoKey(ctx context.Context, req *kmspb.UpdateCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) { + if k.updateCryptoKeyErr != nil { + return nil, k.updateCryptoKeyErr + } + + fck, ok := k.store.fetchFakeCryptoKey(req.CryptoKey.Name) + if !ok { + return nil, fmt.Errorf("could not find CryptoKey %q", req.CryptoKey.Name) + } + + k.mu.Lock() + defer k.mu.Unlock() + + fck.mu.Lock() + defer fck.mu.Unlock() + + fck.CryptoKey = req.CryptoKey + return fck.CryptoKey, nil +} + +func (k *fakeKMSClient) createFakeCryptoKeyVersion(cryptoKey *kmspb.CryptoKey, version string) (*fakeCryptoKeyVersion, error) { + var privateKey crypto.Signer + var testKeys testkey.Keys + + switch cryptoKey.VersionTemplate.Algorithm { + case kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: + privateKey = testKeys.NewEC256(k.t) + case kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384: + privateKey = testKeys.NewEC384(k.t) + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256: + privateKey = testKeys.NewRSA2048(k.t) + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256: + privateKey = testKeys.NewRSA4096(k.t) + default: + return nil, fmt.Errorf("unknown algorithm %q", cryptoKey.VersionTemplate.Algorithm) + } + + pkixData, err := x509.MarshalPKIXPublicKey(privateKey.Public()) + if err != nil { + return nil, err + } + pemCert := new(bytes.Buffer) + if err = pem.Encode(pemCert, &pem.Block{ + Type: "CERTIFICATE", + Bytes: pkixData, + }); err != nil { + return nil, err + } + + return &fakeCryptoKeyVersion{ + privateKey: privateKey, + publicKey: &kmspb.PublicKey{ + Pem: pemCert.String(), + PemCrc32C: &wrapperspb.Int64Value{Value: int64(crc32Checksum(pemCert.Bytes()))}, + }, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Name: path.Join(cryptoKey.Name, "cryptoKeyVersions", version), + State: k.initialCryptoKeyVersionState, + Algorithm: cryptoKey.VersionTemplate.Algorithm, + }, + }, nil +} + +func (k *fakeKMSClient) getDefaultPolicy() *iam.Policy3 { + k.mu.RLock() + defer k.mu.RUnlock() + + policy := new(iam.Policy3) + policy.Bindings = []*iampb.Binding{ + { + Role: "roles/cloudkms.signerVerifier", + Members: []string{fmt.Sprintf("serviceAccount:%s", k.tokeninfo.Email)}, + }, + } + return policy +} + +func (k *fakeKMSClient) putFakeCryptoKeys(fakeCryptoKeys []*fakeCryptoKey) { + for _, fck := range fakeCryptoKeys { + k.store.putFakeCryptoKey(&fakeCryptoKey{ + CryptoKey: fck.CryptoKey, + fakeCryptoKeyVersions: fck.fakeCryptoKeyVersions, + }) + } +} + +func newKMSClientFake(t *testing.T, c *clock.Mock) *fakeKMSClient { + return &fakeKMSClient{ + fakeIAMHandle: &fakeIAMHandle{}, + store: newFakeStore(c), + t: t, + tokeninfo: &oauth2.Tokeninfo{ + Email: "email@example.org", + }, + } +} + +func newFakeStore(c *clock.Mock) fakeStore { + return fakeStore{ + fakeCryptoKeys: make(map[string]*fakeCryptoKey), + clk: c, + } +} diff --git a/pkg/server/plugin/keymanager/gcpkms/fetcher.go b/pkg/server/plugin/keymanager/gcpkms/fetcher.go new file mode 100644 index 0000000000..4070f49f82 --- /dev/null +++ b/pkg/server/plugin/keymanager/gcpkms/fetcher.go @@ -0,0 +1,172 @@ +package gcpkms + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/hashicorp/go-hclog" + keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1" + "golang.org/x/sync/errgroup" + "google.golang.org/api/iterator" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type keyFetcher struct { + keyRing string + kmsClient cloudKeyManagementService + log hclog.Logger + serverID string + tdHash string +} + +// fetchKeyEntries requests Cloud KMS to get the list of CryptoKeys that are +// active in this server. They are returned as a keyEntry array. +func (kf *keyFetcher) fetchKeyEntries(ctx context.Context) ([]*keyEntry, error) { + var keyEntries []*keyEntry + var keyEntriesMutex sync.Mutex + g, ctx := errgroup.WithContext(ctx) + + it := kf.kmsClient.ListCryptoKeys(ctx, &kmspb.ListCryptoKeysRequest{ + Parent: kf.keyRing, + Filter: fmt.Sprintf("labels.%s = %s AND labels.%s = %s AND labels.%s = true", + labelNameServerTD, kf.tdHash, labelNameServerID, kf.serverID, labelNameActive), + }) + for { + cryptoKey, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list SPIRE Server keys in Cloud KMS: %v", err) + } + spireKeyID, ok := getSPIREKeyIDFromCryptoKeyName(cryptoKey.Name) + if !ok { + kf.log.Warn("Could not get SPIRE Key ID from CryptoKey", cryptoKeyNameTag, cryptoKey.Name) + continue + } + + // Trigger a goroutine to get the details of the key + g.Go(func() error { + entries, err := kf.getKeyEntriesFromCryptoKey(ctx, cryptoKey, spireKeyID) + if err != nil { + return err + } + if entries == nil { + return nil + } + + keyEntriesMutex.Lock() + keyEntries = append(keyEntries, entries...) + keyEntriesMutex.Unlock() + return nil + }) + } + + // Wait for all the detail gathering routines to finish. + if err := g.Wait(); err != nil { + statusErr := status.Convert(err) + return nil, status.Errorf(statusErr.Code(), "failed to fetch entries: %v", statusErr.Message()) + } + + return keyEntries, nil +} + +// getKeyEntriesFromCryptoKey builds an array of keyEntry values from the provided +// CryptoKey. In order to do that, Cloud KMS is requested to list the +// CryptoKeyVersions of the CryptoKey. The public key of the CryptoKeyVersion is +// also retrieved from each CryptoKey to construct each keyEntry. +func (kf *keyFetcher) getKeyEntriesFromCryptoKey(ctx context.Context, cryptoKey *kmspb.CryptoKey, spireKeyID string) (keyEntries []*keyEntry, err error) { + if cryptoKey == nil { + return nil, status.Error(codes.Internal, "cryptoKey is nil") + } + + it := kf.kmsClient.ListCryptoKeyVersions(ctx, &kmspb.ListCryptoKeyVersionsRequest{ + Parent: cryptoKey.Name, + // Filter by state, so only enabled keys are returned. This will leave + // out all the versions that have been rotated. + Filter: "state = " + kmspb.CryptoKeyVersion_ENABLED.String(), + }) + for { + cryptoKeyVersion, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + return nil, status.Errorf(codes.Internal, "failure listing CryptoKeyVersions: %v", err) + } + keyType, ok := keyTypeFromCryptoKeyVersionAlgorithm(cryptoKeyVersion.Algorithm) + if !ok { + return nil, status.Errorf(codes.Internal, "unsupported CryptoKeyVersionAlgorithm: %v", cryptoKeyVersion.Algorithm) + } + + pubKey, err := getPublicKeyFromCryptoKeyVersion(ctx, kf.log, kf.kmsClient, cryptoKeyVersion.Name) + if err != nil { + return nil, status.Errorf(codes.Internal, "error getting public key: %v", err) + } + + keyEntry := &keyEntry{ + cryptoKey: cryptoKey, + cryptoKeyVersionName: cryptoKeyVersion.Name, + publicKey: &keymanagerv1.PublicKey{ + Id: spireKeyID, + Type: keyType, + PkixData: pubKey, + Fingerprint: makeFingerprint(pubKey), + }, + } + + keyEntries = append(keyEntries, keyEntry) + } + + return keyEntries, nil +} + +// getSPIREKeyIDFromCryptoKeyName parses a CryptoKey resource name to get the +// SPIRE Key ID. This Key ID is used in the Server KeyManager interface. +func getSPIREKeyIDFromCryptoKeyName(cryptoKeyName string) (string, bool) { + // cryptoKeyName is the resource name for the CryptoKey holding the SPIRE Key + // in the format: projects/*/locations/*/keyRings/*/cryptoKeys/spire-key-*-*. + // Example: projects/project-name/locations/us-east1/keyRings/key-ring-name/cryptoKeys/spire-key-1f2e225a-91d8-4589-a4fe-f88b7bb04bac-x509-CA-A + + // Get the last element of the path. + i := strings.LastIndex(cryptoKeyName, "/") + if i < 0 { + // All CryptoKeys are under a Key Ring; not a valid Crypto Key name. + return "", false + } + + // The i index will indicate us where + // "spire-key-1f2e225a-91d8-4589-a4fe-f88b7bb04bac-x509-CA-A" starts. + // Now we have to get the position where the SPIRE Key ID starts. + // For that, we need to add the length of the CryptoKey name prefix that we + // are using, the UUID length, and the two "-" separators used in our format. + spireKeyIDIndex := i + len(cryptoKeyNamePrefix) + 39 // 39 is the UUID length plus two '-' separators + if spireKeyIDIndex >= len(cryptoKeyName) { + // The index is out of range. + return "", false + } + spireKeyID := cryptoKeyName[spireKeyIDIndex:] + return spireKeyID, true +} + +// keyTypeFromCryptoKeyVersionAlgorithm gets the KeyType that corresponds to the +// given CryptoKeyVersion_CryptoKeyVersionAlgorithm. +func keyTypeFromCryptoKeyVersionAlgorithm(algorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm) (keymanagerv1.KeyType, bool) { + switch algorithm { + case kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: + return keymanagerv1.KeyType_EC_P256, true + case kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384: + return keymanagerv1.KeyType_EC_P384, true + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256: + return keymanagerv1.KeyType_RSA_2048, true + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256: + return keymanagerv1.KeyType_RSA_4096, true + default: + return keymanagerv1.KeyType_UNSPECIFIED_KEY_TYPE, false + } +} diff --git a/pkg/server/plugin/keymanager/gcpkms/gcpkms.go b/pkg/server/plugin/keymanager/gcpkms/gcpkms.go new file mode 100644 index 0000000000..39d7c55cdc --- /dev/null +++ b/pkg/server/plugin/keymanager/gcpkms/gcpkms.go @@ -0,0 +1,1126 @@ +package gcpkms + +import ( + "context" + "crypto/sha1" //nolint: gosec // We use sha1 to hash trust domain names in 128 bytes to avoid label value restrictions + "crypto/sha256" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "hash/crc32" + "os" + "strings" + "sync" + "time" + + "cloud.google.com/go/iam" + "cloud.google.com/go/iam/apiv1/iampb" + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/andres-erbsen/clock" + "github.com/gofrs/uuid" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/hcl" + keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1" + configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" + "github.com/spiffe/spire/pkg/common/catalog" + "github.com/spiffe/spire/pkg/common/diskutil" + "google.golang.org/api/iterator" + "google.golang.org/api/option" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/fieldmaskpb" +) + +const ( + pluginName = "gcp_kms" + + algorithmTag = "algorithm" + cryptoKeyNameTag = "crypto_key_name" + cryptoKeyVersionNameTag = "crypto_key_version_name" + cryptoKeyVersionStateTag = "crypto_key_version_state" + scheduledDestroyTimeTag = "scheduled_destroy_time" + reasonTag = "reason" + + disposeCryptoKeysFrequency = time.Hour * 48 + keepActiveCryptoKeysFrequency = time.Hour * 6 + maxStaleDuration = time.Hour * 24 * 14 // Two weeks. + + cryptoKeyNamePrefix = "spire-key" + labelNameServerID = "spire-server-id" + labelNameLastUpdate = "spire-last-update" + labelNameServerTD = "spire-server-td" + labelNameActive = "spire-active" + + getPublicKeyMaxAttempts = 10 +) + +func BuiltIn() catalog.BuiltIn { + return builtin(New()) +} + +func builtin(p *Plugin) catalog.BuiltIn { + return catalog.MakeBuiltIn(pluginName, + keymanagerv1.KeyManagerPluginServer(p), + configv1.ConfigServiceServer(p), + ) +} + +type keyEntry struct { + cryptoKey *kmspb.CryptoKey + cryptoKeyVersionName string + publicKey *keymanagerv1.PublicKey +} + +type pluginHooks struct { + newKMSClient func(context.Context, ...option.ClientOption) (cloudKeyManagementService, error) + + clk clock.Clock + + // Used for testing only. + disposeCryptoKeysSignal chan error + enqueueDestructionSignal chan error + keepActiveCryptoKeysSignal chan error + scheduleDestroySignal chan error + setInactiveSignal chan error +} + +type pluginData struct { + customPolicy *iam.Policy3 + serverID string + tdHash string +} + +// Plugin is the main representation of this keymanager plugin. +type Plugin struct { + keymanagerv1.UnsafeKeyManagerServer + configv1.UnsafeConfigServer + + cancelTasks context.CancelFunc + + config *Config + configMtx sync.RWMutex + + entries map[string]keyEntry + entriesMtx sync.RWMutex + + pd *pluginData + pdMtx sync.RWMutex + + hooks pluginHooks + kmsClient cloudKeyManagementService + log hclog.Logger + scheduleDestroy chan string +} + +// Config provides configuration context for the plugin. +type Config struct { + // File path location where key metadata used by the plugin is persisted. + KeyMetadataFile string `hcl:"key_metadata_file" json:"key_metadata_file"` + + // File path location to a custom IAM Policy (v3) that will be set to + // created CryptoKeys. + KeyPolicyFile string `hcl:"key_policy_file" json:"key_policy_file"` + + // KeyRing is the resource ID of the key ring where the keys managed by this + // plugin reside, in the format projects/*/locations/*/keyRings/*. + KeyRing string `hcl:"key_ring" json:"key_ring"` + + // Path to the service account file used to authenticate with the Cloud KMS + // API. If not specified, the value of the GOOGLE_APPLICATION_CREDENTIALS + // environment variable is used. + ServiceAccountFile string `hcl:"service_account_file" json:"service_account_file"` +} + +// New returns an instantiated plugin. +func New() *Plugin { + return newPlugin(newKMSClient) +} + +// newPlugin returns a new plugin instance. +func newPlugin( + newKMSClient func(context.Context, ...option.ClientOption) (cloudKeyManagementService, error), +) *Plugin { + return &Plugin{ + entries: make(map[string]keyEntry), + hooks: pluginHooks{ + newKMSClient: newKMSClient, + clk: clock.New(), + }, + scheduleDestroy: make(chan string, 120), + } +} + +func (p *Plugin) Close() error { + if p.kmsClient == nil { + return nil + } + p.log.Debug("Closing the connection to the Cloud KMS API service") + return p.kmsClient.Close() +} + +// Configure sets up the plugin. +func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { + config, err := parseAndValidateConfig(req.HclConfiguration) + if err != nil { + return nil, err + } + + serverID, err := getOrCreateServerID(config.KeyMetadataFile) + if err != nil { + return nil, err + } + p.log.Debug("Loaded server ID", "server_id", serverID) + var customPolicy *iam.Policy3 + if config.KeyPolicyFile != "" { + if customPolicy, err = parsePolicyFile(config.KeyPolicyFile); err != nil { + return nil, status.Errorf(codes.Internal, "could not parse policy file: %v", err) + } + } + + // Label values do not allow "." and have a maximum length of 63 characters. + // https://cloud.google.com/kms/docs/creating-managing-labels#requirements + // Hash the trust domain name to avoid restrictions. + tdHashBytes := sha1.Sum([]byte(req.CoreConfiguration.TrustDomain)) //nolint: gosec // We use sha1 to hash trust domain names in 128 bytes to avoid label restrictions + tdHashString := hex.EncodeToString(tdHashBytes[:]) + + p.setPluginData(&pluginData{ + customPolicy: customPolicy, + serverID: serverID, + tdHash: tdHashString, + }) + + var opts []option.ClientOption + if config.ServiceAccountFile != "" { + opts = append(opts, option.WithCredentialsFile(config.ServiceAccountFile)) + } + + kc, err := p.hooks.newKMSClient(ctx, opts...) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create Google Cloud KMS client: %v", err) + } + + fetcher := &keyFetcher{ + keyRing: config.KeyRing, + kmsClient: kc, + log: p.log, + serverID: serverID, + tdHash: tdHashString, + } + p.log.Debug("Fetching keys from Cloud KMS", "key_ring", config.KeyRing) + keyEntries, err := fetcher.fetchKeyEntries(ctx) + if err != nil { + return nil, err + } + + p.setCache(keyEntries) + p.kmsClient = kc + + // Cancel previous tasks in case of re configure. + if p.cancelTasks != nil { + p.cancelTasks() + } + + p.configMtx.Lock() + defer p.configMtx.Unlock() + p.config = config + + // Start long-running tasks. + ctx, p.cancelTasks = context.WithCancel(context.Background()) + go p.scheduleDestroyTask(ctx) + go p.keepActiveCryptoKeysTask(ctx) + go p.disposeCryptoKeysTask(ctx) + + return &configv1.ConfigureResponse{}, nil +} + +// GenerateKey creates a key in KMS. If a key already exists in the local storage, +// it is updated. +func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyRequest) (*keymanagerv1.GenerateKeyResponse, error) { + if req.KeyId == "" { + return nil, status.Error(codes.InvalidArgument, "key id is required") + } + if req.KeyType == keymanagerv1.KeyType_UNSPECIFIED_KEY_TYPE { + return nil, status.Error(codes.InvalidArgument, "key type is required") + } + + pubKey, err := p.createKey(ctx, req.KeyId, req.KeyType) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to generate key: %v", err) + } + + return &keymanagerv1.GenerateKeyResponse{ + PublicKey: pubKey, + }, nil +} + +// GetPublicKey returns the public key for a given key +func (p *Plugin) GetPublicKey(ctx context.Context, req *keymanagerv1.GetPublicKeyRequest) (*keymanagerv1.GetPublicKeyResponse, error) { + if req.KeyId == "" { + return nil, status.Error(codes.InvalidArgument, "key id is required") + } + + entry, ok := p.getKeyEntry(req.KeyId) + if !ok { + return nil, status.Errorf(codes.NotFound, "key %q not found", req.KeyId) + } + + return &keymanagerv1.GetPublicKeyResponse{ + PublicKey: entry.publicKey, + }, nil +} + +// GetPublicKeys returns the publicKey for all the keys. +func (p *Plugin) GetPublicKeys(context.Context, *keymanagerv1.GetPublicKeysRequest) (*keymanagerv1.GetPublicKeysResponse, error) { + var keys []*keymanagerv1.PublicKey + p.entriesMtx.RLock() + defer p.entriesMtx.RUnlock() + for _, key := range p.entries { + keys = append(keys, key.publicKey) + } + + return &keymanagerv1.GetPublicKeysResponse{PublicKeys: keys}, nil +} + +// SetLogger sets a logger. +func (p *Plugin) SetLogger(log hclog.Logger) { + p.log = log +} + +// SignData creates a digital signature for the data to be signed. +func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest) (*keymanagerv1.SignDataResponse, error) { + if req.KeyId == "" { + return nil, status.Error(codes.InvalidArgument, "key id is required") + } + if req.SignerOpts == nil { + return nil, status.Error(codes.InvalidArgument, "signer opts is required") + } + + keyEntry, hasKey := p.getKeyEntry(req.KeyId) + if !hasKey { + return nil, status.Errorf(codes.NotFound, "key %q not found", req.KeyId) + } + + var ( + hashAlgo keymanagerv1.HashAlgorithm + digest *kmspb.Digest + ) + switch opts := req.SignerOpts.(type) { + case *keymanagerv1.SignDataRequest_HashAlgorithm: + hashAlgo = opts.HashAlgorithm + case *keymanagerv1.SignDataRequest_PssOptions: + // RSASSA-PSS is not supported by this plugin. + // See the comment in cryptoKeyVersionAlgorithmFromKeyType function for + // more details. + return nil, status.Error(codes.InvalidArgument, "the only RSA signature scheme supported is RSASSA-PKCS1-v1_5") + default: + return nil, status.Errorf(codes.InvalidArgument, "unsupported signer opts type %T", opts) + } + switch { + case hashAlgo == keymanagerv1.HashAlgorithm_UNSPECIFIED_HASH_ALGORITHM: + return nil, status.Error(codes.InvalidArgument, "hash algorithm is required") + case hashAlgo == keymanagerv1.HashAlgorithm_SHA256: + digest = &kmspb.Digest{ + Digest: &kmspb.Digest_Sha256{Sha256: req.Data}, + } + case hashAlgo == keymanagerv1.HashAlgorithm_SHA384: + digest = &kmspb.Digest{ + Digest: &kmspb.Digest_Sha384{Sha384: req.Data}, + } + default: + return nil, status.Error(codes.InvalidArgument, "hash algorithm not supported") + } + + signResp, err := p.kmsClient.AsymmetricSign(ctx, &kmspb.AsymmetricSignRequest{ + Name: keyEntry.cryptoKeyVersionName, + Digest: digest, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to sign: %v", err) + } + + // Perform integrity verification. + if int64(crc32Checksum(signResp.Signature)) != signResp.SignatureCrc32C.Value { + return nil, status.Error(codes.Internal, "error signing: response corrupted in-transit") + } + + return &keymanagerv1.SignDataResponse{ + Signature: signResp.Signature, + KeyFingerprint: keyEntry.publicKey.Fingerprint, + }, nil +} + +// createKey creates a new CryptoKey with a new CryptoKeyVersion in Cloud KMS +// if there is not already a cached entry with the specified SPIRE Key ID. +// If the cache already has an entry with this SPIRE Key ID, a new +// CryptoKeyVersion is added to the corresponding CryptoKey in Cloud KMS and the +// old CryptoKeyVersion is enqueued for destruction. +// If there is a specified IAM policy through the KeyPolicyFile configuration, +// that policy is set to the created CryptoKey. If there is no IAM policy specified, +// a default policy is constructed and attached. This function requests Cloud KMS +// to get the public key of the created CryptoKeyVersion. A keyEntry is returned +// with the CryptoKey, CryptoKeyVersion and public key. +func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keymanagerv1.KeyType) (*keymanagerv1.PublicKey, error) { + // If we already have this SPIRE Key ID cached, a new CryptoKeyVersion is + // added to the existing CryptoKey and the cache is updated. The old + // CryptoKeyVersion is enqueued for destruction. + if entry, ok := p.getKeyEntry(spireKeyID); ok { + return p.addCryptoKeyVersionToCachedEntry(ctx, entry, spireKeyID, keyType) + } + + algorithm, err := cryptoKeyVersionAlgorithmFromKeyType(keyType) + if err != nil { + return nil, err + } + + cryptoKeyID, err := p.generateCryptoKeyID(spireKeyID) + if err != nil { + return nil, fmt.Errorf("could not generate CryptoKeyID: %w", err) + } + + cryptoKeyLabels, err := p.getCryptoKeyLabels() + if err != nil { + return nil, status.Errorf(codes.Internal, "could not get CryptoKey labels: %v", err) + } + + config, err := p.getConfig() + if err != nil { + return nil, err + } + + cryptoKey, err := p.kmsClient.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ + CryptoKey: &kmspb.CryptoKey{ + Labels: cryptoKeyLabels, + Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ + Algorithm: algorithm, + }, + }, + CryptoKeyId: cryptoKeyID, + Parent: config.KeyRing, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create CryptoKey: %v", err) + } + + log := p.log.With(cryptoKeyNameTag, cryptoKey.Name) + log.Debug("CryptoKey created", algorithmTag, algorithm) + + if err := p.setIamPolicy(ctx, cryptoKey.Name); err != nil { + log.Debug("Failed to set IAM policy") + return nil, status.Errorf(codes.Internal, "failed to set IAM policy: %v", err) + } + + cryptoKeyVersionName := cryptoKey.Name + "/cryptoKeyVersions/1" + log.Debug("CryptoKeyVersion version added", cryptoKeyVersionNameTag, cryptoKeyVersionName) + + pubKey, err := getPublicKeyFromCryptoKeyVersion(ctx, p.log, p.kmsClient, cryptoKeyVersionName) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get public key: %v", err) + } + newKeyEntry := keyEntry{ + cryptoKey: cryptoKey, + cryptoKeyVersionName: cryptoKeyVersionName, + publicKey: &keymanagerv1.PublicKey{ + Id: spireKeyID, + Type: keyType, + PkixData: pubKey, + Fingerprint: makeFingerprint(pubKey), + }, + } + + p.setKeyEntry(spireKeyID, newKeyEntry) + return newKeyEntry.publicKey, nil +} + +// addCryptoKeyVersionToCachedEntry adds a new CryptoKeyVersion to an existing +// CryptoKey, updating the cached entries. +func (p *Plugin) addCryptoKeyVersionToCachedEntry(ctx context.Context, entry keyEntry, spireKeyID string, keyType keymanagerv1.KeyType) (*keymanagerv1.PublicKey, error) { + algorithm, err := cryptoKeyVersionAlgorithmFromKeyType(keyType) + if err != nil { + return nil, err + } + + log := p.log.With(cryptoKeyNameTag, entry.cryptoKey.Name) + + // Check if the algorithm has changed and update if needed. + if entry.cryptoKey.VersionTemplate.Algorithm != algorithm { + entry.cryptoKey.VersionTemplate.Algorithm = algorithm + _, err := p.kmsClient.UpdateCryptoKey(ctx, &kmspb.UpdateCryptoKeyRequest{ + CryptoKey: entry.cryptoKey, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"version_template.algorithm"}, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to update CryptoKey with updated algorithm: %w", err) + } + log.Debug("CryptoKey updated", algorithmTag, algorithm) + } + cryptoKeyVersion, err := p.kmsClient.CreateCryptoKeyVersion(ctx, &kmspb.CreateCryptoKeyVersionRequest{ + Parent: entry.cryptoKey.Name, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to create CryptoKeyVersion: %w", err) + } + log.Debug("CryptoKeyVersion added", cryptoKeyVersionNameTag, cryptoKeyVersion.Name) + + pubKey, err := getPublicKeyFromCryptoKeyVersion(ctx, p.log, p.kmsClient, cryptoKeyVersion.Name) + if err != nil { + return nil, fmt.Errorf("failed to get public key: %w", err) + } + + newKeyEntry := keyEntry{ + cryptoKey: entry.cryptoKey, + cryptoKeyVersionName: cryptoKeyVersion.Name, + publicKey: &keymanagerv1.PublicKey{ + Id: spireKeyID, + Type: keyType, + PkixData: pubKey, + Fingerprint: makeFingerprint(pubKey), + }, + } + + p.setKeyEntry(spireKeyID, newKeyEntry) + + if err := p.enqueueDestruction(entry.cryptoKeyVersionName); err != nil { + log.Error("Failed to enqueue CryptoKeyVersion for destruction", reasonTag, err) + } + + return newKeyEntry.publicKey, nil +} + +// disposeCryptoKeys looks for active CryptoKeys that haven't been updated +// during the maxStaleDuration time window. Those keys are then enqueued for +// destruction. +func (p *Plugin) disposeCryptoKeys(ctx context.Context) error { + p.log.Debug("Looking for CryptoKeys to dispose") + + config, err := p.getConfig() + if err != nil { + return err + } + + disposeCryptoKeysFilter, err := p.getDisposeCryptoKeysFilter() + if err != nil { + return err + } + itCryptoKeys := p.kmsClient.ListCryptoKeys(ctx, &kmspb.ListCryptoKeysRequest{ + Parent: config.KeyRing, + Filter: disposeCryptoKeysFilter, + }) + + for { + cryptoKey, err := itCryptoKeys.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + p.log.Error("Failure listing CryptoKeys to dispose", reasonTag, err) + return err + } + + itCryptoKeyVersions := p.kmsClient.ListCryptoKeyVersions(ctx, &kmspb.ListCryptoKeyVersionsRequest{ + Parent: cryptoKey.Name, + Filter: "state = " + kmspb.CryptoKeyVersion_ENABLED.String(), + }) + + // If the CryptoKey doesn't have any enabled CryptoKeyVersion, mark it + // as inactive so it's not returned future calls. + cryptoKeyVersion, err := itCryptoKeyVersions.Next() + if errors.Is(err, iterator.Done) { + p.setInactive(ctx, cryptoKey) + continue + } + + for { + if err != nil { + p.log.Error("Failure listing CryptoKeyVersios", reasonTag, err) + return err + } + + if err := p.enqueueDestruction(cryptoKeyVersion.Name); err != nil { + p.log.With(cryptoKeyNameTag, cryptoKey.Name).Error("Failed to enqueue CryptoKeyVersion for destruction", reasonTag, err) + } + + cryptoKeyVersion, err = itCryptoKeyVersions.Next() + if errors.Is(err, iterator.Done) { + // No more enabled CryptoKeyVersions in this CryptoKey. + break + } + } + } + return nil +} + +// disposeCryptoKeysTask will be run every 24hs. +// It will schedule the destruction of CryptoKeyVersions that have a +// spire-last-update label value older than two weeks. +// It will only schedule the destruction of CryptoKeyVersions belonging to the +// current trust domain but not the current server. The spire-server-td and +// spire-server-id labels are used to identify the trust domain and server. +func (p *Plugin) disposeCryptoKeysTask(ctx context.Context) { + ticker := p.hooks.clk.Ticker(disposeCryptoKeysFrequency) + defer ticker.Stop() + + p.notifyDisposeCryptoKeys(nil) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := p.disposeCryptoKeys(ctx) + p.notifyDisposeCryptoKeys(err) + } + } +} + +// enqueueDestruction enqueues the specified CryptoKeyVersion for destruction. +func (p *Plugin) enqueueDestruction(cryptoKeyVersionName string) (err error) { + select { + case p.scheduleDestroy <- cryptoKeyVersionName: + p.log.Debug("CryptoKeyVersion enqueued for destruction", cryptoKeyVersionNameTag, cryptoKeyVersionName) + default: + err = fmt.Errorf("could not enqueue CryptoKeyVersion %q for destruction", cryptoKeyVersionName) + } + + p.notifyEnqueueDestruction(err) + return err +} + +// getAuthenticatedServiceAccount gets the email of the authenticated service +// account that is interacting with the Cloud KMS Service. +func (p *Plugin) getAuthenticatedServiceAccount() (email string, err error) { + tokenInfo, err := p.kmsClient.GetTokeninfo() + if err != nil { + return "", fmt.Errorf("could not get token information: %w", err) + } + + if tokenInfo.Email == "" { + return "", errors.New("could not get email of authenticated service account; email is empty") + } + return tokenInfo.Email, nil +} + +// getConfig gets the configuration of the plugin. +func (p *Plugin) getConfig() (*Config, error) { + p.configMtx.RLock() + defer p.configMtx.RUnlock() + + if p.config == nil { + return nil, status.Error(codes.FailedPrecondition, "not configured") + } + + return p.config, nil +} + +// getCryptoKeyLabels gets the labels that must be set to a new CryptoKey +// that is being created. +func (p *Plugin) getCryptoKeyLabels() (map[string]string, error) { + pd, err := p.getPluginData() + if err != nil { + return nil, err + } + return map[string]string{ + labelNameServerTD: pd.tdHash, + labelNameServerID: pd.serverID, + labelNameActive: "true", + }, nil +} + +// getDisposeCryptoKeysFilter gets the filter to be used to get the list of +// CryptoKeys that are stale but are still marked as active. +func (p *Plugin) getDisposeCryptoKeysFilter() (string, error) { + now := p.hooks.clk.Now() + pd, err := p.getPluginData() + if err != nil { + return "", err + } + return fmt.Sprintf("labels.%s = %s AND labels.%s != %s AND labels.%s = true AND labels.%s < %d", + labelNameServerTD, pd.tdHash, labelNameServerID, pd.serverID, labelNameActive, labelNameLastUpdate, now.Add(-maxStaleDuration).Unix()), nil +} + +// getKeyEntry gets the entry from the cache that matches the provided +// SPIRE Key ID +func (p *Plugin) getKeyEntry(keyID string) (ke keyEntry, ok bool) { + p.entriesMtx.RLock() + defer p.entriesMtx.RUnlock() + + ke, ok = p.entries[keyID] + return ke, ok +} + +// getPluginData gets the pluginData structure maintained by the plugin. +func (p *Plugin) getPluginData() (*pluginData, error) { + p.pdMtx.RLock() + defer p.pdMtx.RUnlock() + + if p.pd == nil { + return nil, status.Error(codes.FailedPrecondition, "plugin data not yet initialized") + } + return p.pd, nil +} + +// setIamPolicy sets the IAM policy specified in the KeyPolicyFile to the given +// resource. If there is no KeyPolicyFile specified, a default policy is constructed +// and set to the resource. +func (p *Plugin) setIamPolicy(ctx context.Context, cryptoKeyName string) (err error) { + log := p.log.With(cryptoKeyNameTag, cryptoKeyName) + + // Get the handle to be able to inspect and change the policy of the + // CryptoKey. + h := p.kmsClient.ResourceIAM(cryptoKeyName) + if h == nil { + return errors.New("could not get Cloud KMS Handle") + } + + // We use V3 for policies. + h3 := h.V3() + if h3 == nil { + return errors.New("could not get Cloud KMS Handle3") + } + + // Get the policy. + policy, err := h3.Policy(ctx) + if err != nil { + return fmt.Errorf("failed to retrieve IAM policy: %w", err) + } + + // We expect the policy to be empty. + if len(policy.Bindings) > 0 { + // The policy is not empty, log the situation and do not replace it. + log.Warn("The CryptoKey already has a policy. No policy will be set.") + return nil + } + pd, err := p.getPluginData() + if err != nil { + return err + } + + if pd.customPolicy != nil { + // There is a custom policy defined. + if err := h3.SetPolicy(ctx, pd.customPolicy); err != nil { + return fmt.Errorf("failed to set custom IAM policy: %w", err) + } + log.Debug("IAM policy updated to use custom policy") + return nil + } + + // No custom policy defined. Build the default policy. + serviceAccount, err := p.getAuthenticatedServiceAccount() + if err != nil { + return status.Errorf(codes.Internal, "failed to get current identity: %v", err) + } + policy.Bindings = []*iampb.Binding{ + { + Role: "roles/cloudkms.signerVerifier", + Members: []string{fmt.Sprintf("serviceAccount:%s", serviceAccount)}, + }, + } + if err := h3.SetPolicy(ctx, policy); err != nil { + return fmt.Errorf("failed to set default IAM policy: %w", err) + } + log.Debug("IAM policy updated to use default policy") + return nil +} + +// setKeyEntry gets the entry from the cache that matches the provided +// SPIRE Key ID +func (p *Plugin) setKeyEntry(keyID string, ke keyEntry) { + p.entriesMtx.Lock() + defer p.entriesMtx.Unlock() + + p.entries[keyID] = ke +} + +// setPluginData sets the pluginData structure maintained by the plugin. +func (p *Plugin) setPluginData(pd *pluginData) { + p.pdMtx.Lock() + defer p.pdMtx.Unlock() + + p.pd = pd +} + +// keepActiveCryptoKeys keeps CryptoKeys managed by this plugin active updating +// the spire-last-update label with the current Unix time. +func (p *Plugin) keepActiveCryptoKeys(ctx context.Context) error { + p.log.Debug("Keeping CryptoKeys managed by this server active") + + p.entriesMtx.Lock() + defer p.entriesMtx.Unlock() + var errs []string + for _, entry := range p.entries { + entry.cryptoKey.Labels[labelNameLastUpdate] = fmt.Sprint(p.hooks.clk.Now().Unix()) + _, err := p.kmsClient.UpdateCryptoKey(ctx, &kmspb.UpdateCryptoKeyRequest{ + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"labels"}, + }, + CryptoKey: entry.cryptoKey, + }) + if err != nil { + p.log.Error("Failed to update CryptoKey", cryptoKeyNameTag, entry.cryptoKey.Name, reasonTag, err) + errs = append(errs, err.Error()) + } + } + + if errs != nil { + return fmt.Errorf(strings.Join(errs, "; ")) + } + return nil +} + +// keepActiveCryptoKeysTask updates the CryptoKeys in the cache every 6 hours, +// setting the spire-last-update label to the current (Unix) time. +// This is done to be able to detect CryptoKeys that are inactive (not in use +// by any server). +func (p *Plugin) keepActiveCryptoKeysTask(ctx context.Context) { + ticker := p.hooks.clk.Ticker(keepActiveCryptoKeysFrequency) + defer ticker.Stop() + + p.notifyKeepActiveCryptoKeys(nil) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := p.keepActiveCryptoKeys(ctx) + p.notifyKeepActiveCryptoKeys(err) + } + } +} + +func (p *Plugin) notifyDestroy(err error) { + if p.hooks.scheduleDestroySignal != nil { + p.hooks.scheduleDestroySignal <- err + } +} + +func (p *Plugin) notifyDisposeCryptoKeys(err error) { + if p.hooks.disposeCryptoKeysSignal != nil { + p.hooks.disposeCryptoKeysSignal <- err + } +} + +func (p *Plugin) notifyEnqueueDestruction(err error) { + if p.hooks.enqueueDestructionSignal != nil { + p.hooks.enqueueDestructionSignal <- err + } +} + +func (p *Plugin) notifySetInactive(err error) { + if p.hooks.setInactiveSignal != nil { + p.hooks.setInactiveSignal <- err + } +} + +func (p *Plugin) notifyKeepActiveCryptoKeys(err error) { + if p.hooks.keepActiveCryptoKeysSignal != nil { + p.hooks.keepActiveCryptoKeysSignal <- err + } +} + +// scheduleDestroyTask is a long running task that schedules the destruction +// of inactive CryptoKeyVersions and sets the corresponding CryptoKey as inactive. +func (p *Plugin) scheduleDestroyTask(ctx context.Context) { + backoffMin := 1 * time.Second + backoffMax := 60 * time.Second + backoff := backoffMin + + for { + select { + case <-ctx.Done(): + return + case cryptoKeyVersionName := <-p.scheduleDestroy: + log := p.log.With(cryptoKeyVersionNameTag, cryptoKeyVersionName) + destroyedCryptoKeyVersion, err := p.kmsClient.DestroyCryptoKeyVersion(ctx, &kmspb.DestroyCryptoKeyVersionRequest{ + Name: cryptoKeyVersionName, + }) + switch status.Code(err) { + case codes.NotFound: + // CryptoKeyVersion is not found, no CryptoKeyVersion to destroy + log.Warn("CryptoKeyVersion not found") + backoff = backoffMin + p.notifyDestroy(err) + continue + case codes.OK: + log.Debug("CryptoKeyVersion scheduled for destruction", scheduledDestroyTimeTag, destroyedCryptoKeyVersion.DestroyTime.AsTime()) + backoff = backoffMin + p.notifyDestroy(nil) + continue + default: + log.Error("It was not possible to schedule CryptoKeyVersion for destruction", reasonTag, err) + + // There was an error in the DestroyCryptoKeyVersion call. + // Try to get the CryptoKeyVersion to know the state of the + // CryptoKeyVersion and if we need to re-enqueue. + cryptoKeyVersion, err := p.kmsClient.GetCryptoKeyVersion(ctx, &kmspb.GetCryptoKeyVersionRequest{ + Name: cryptoKeyVersionName, + }) + switch status.Code(err) { + case codes.NotFound: + // Purely defensive. We don't really expect this situation, + // because this should have been captured during the + // DestroyCryptoKeyVersion call that was just performed. + log.Warn("CryptoKeyVersion not found") + backoff = backoffMin + p.notifyDestroy(err) + continue + case codes.OK: + if cryptoKeyVersion.State != kmspb.CryptoKeyVersion_ENABLED { + // Something external to the plugin modified the state + // of the CryptoKeyVersion. Do not try to schedule it for + // destruction. + log.Warn("CryptoKeyVersion is not enabled, will not be scheduled for destruction", cryptoKeyVersionStateTag, cryptoKeyVersion.State.String()) + backoff = backoffMin + p.notifyDestroy(err) + continue + } + default: + // The GetCryptoKeyVersion call failed. Log this and re-enqueue + // the CryptoKey for destruction. Hopefully, this is a + // recoverable error. + log.Error("Could not get the CryptoKeyVersion while trying to schedule it for destruction", reasonTag, err) + } + + select { + case p.scheduleDestroy <- cryptoKeyVersionName: + log.Debug("CryptoKeyVersion re-enqueued for destruction") + default: + log.Error("Failed to re-enqueue CryptoKeyVersion for destruction") + } + } + p.notifyDestroy(err) + backoff = min(backoff*2, backoffMax) + p.hooks.clk.Sleep(backoff) + } + } +} + +// setInactive updates the spire-active label in the specified CryptoKey to +// indicate that is inactive. +func (p *Plugin) setInactive(ctx context.Context, cryptoKey *kmspb.CryptoKey) { + log := p.log.With(cryptoKeyNameTag, cryptoKey.Name) + + cryptoKey.Labels[labelNameActive] = "false" + _, err := p.kmsClient.UpdateCryptoKey(ctx, &kmspb.UpdateCryptoKeyRequest{ + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"labels"}, + }, + CryptoKey: cryptoKey, + }) + if err != nil { + log.Error("Could not update CryptoKey as incactive", reasonTag, err) + } + + log.Debug("CryptoKey updated as inactive", cryptoKeyNameTag, cryptoKey.Name) + p.notifySetInactive(err) +} + +// setCache sets the cached entries with the provided entries. +func (p *Plugin) setCache(keyEntries []*keyEntry) { + p.entriesMtx.Lock() + defer p.entriesMtx.Unlock() + + p.entries = make(map[string]keyEntry) + + for _, e := range keyEntries { + p.entries[e.publicKey.Id] = *e + p.log.Debug("Cloud KMS key loaded", cryptoKeyVersionNameTag, e.cryptoKeyVersionName, algorithmTag, e.cryptoKey.VersionTemplate.Algorithm) + } +} + +// createServerID creates a randomly generated UUID to be used as a server ID +// and stores it in the specified idPath. +func createServerID(idPath string) (string, error) { + id, err := generateUniqueID() + if err != nil { + return "", status.Errorf(codes.Internal, "failed to generate ID for server: %v", err) + } + + err = diskutil.WritePrivateFile(idPath, []byte(id)) + if err != nil { + return "", status.Errorf(codes.Internal, "failed to persist server ID on path: %v", err) + } + return id, nil +} + +// cryptoKeyVersionAlgorithmFromKeyType gets the corresponding algorithm of the +// CryptoKeyVersion from the provided key type. +// The returned CryptoKeyVersion_CryptoKeyVersionAlgorithm indicates the +// parameters that must be used for signing. +func cryptoKeyVersionAlgorithmFromKeyType(keyType keymanagerv1.KeyType) (kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm, error) { + // CryptoKeyVersion_CryptoKeyVersionAlgorithm specifies the padding algorithm + // and the digest algorithm for RSA signatures. The key type in the Key + // Manager interface does not contain the information about these parameters + // for signing. Currently, there is no way in SPIRE to specify custom + // parameters when signing through the ca.ServerCA interface and + // x509.CreateCertificate defaults to RSASSA-PKCS-v1_5 as the padding + // algorithm and a SHA256 digest. Therefore, for RSA signing keys we + // choose the corresponding CryptoKeyVersion_CryptoKeyVersionAlgorithm using + // RSASSA-PKCS-v1_5 for padding and a SHA256 digest. + switch { + case keyType == keymanagerv1.KeyType_EC_P256: + return kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, nil + case keyType == keymanagerv1.KeyType_EC_P384: + return kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, nil + case keyType == keymanagerv1.KeyType_RSA_2048: + return kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256, nil + case keyType == keymanagerv1.KeyType_RSA_4096: + return kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, nil + default: + return kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED, fmt.Errorf("unsupported key type %q", keyType) + } +} + +// generateCryptoKeyID returns a new identifier to be used as a CryptoKeyID. +// The returned identifier has the form: spire-key--, +// where UUID is a new randomly generated UUID and SPIRE-KEY-ID is provided +// through the spireKeyID paramenter. +func (p *Plugin) generateCryptoKeyID(spireKeyID string) (cryptoKeyID string, err error) { + pd, err := p.getPluginData() + if err != nil { + return "", err + } + return fmt.Sprintf("%s-%s-%s", cryptoKeyNamePrefix, pd.serverID, spireKeyID), nil +} + +// crc32Checksum returns the CRC-32 checksum of data using the polynomial +// represented by the table constructed from the specified data. +// This is used to perform integrity verification of the result when that's +// available in the Cloud Key Management Service API. +// https://cloud.google.com/kms/docs/data-integrity-guidelines +func crc32Checksum(data []byte) uint32 { + t := crc32.MakeTable(crc32.Castagnoli) + return crc32.Checksum(data, t) +} + +// generateUniqueID returns a randomly generated UUID. +func generateUniqueID() (id string, err error) { + u, err := uuid.NewV4() + if err != nil { + return "", status.Errorf(codes.Internal, "could not create a randomly generated UUID: %v", err) + } + + return u.String(), nil +} + +// getOrCreateServerID gets the server ID from the specified file path or creates +// a new server ID if the file does not exist. +func getOrCreateServerID(idPath string) (string, error) { + data, err := os.ReadFile(idPath) + switch { + case errors.Is(err, os.ErrNotExist): + return createServerID(idPath) + case err != nil: + return "", status.Errorf(codes.Internal, "failed to read server ID from path: %v", err) + } + + serverID, err := uuid.FromString(string(data)) + if err != nil { + return "", status.Errorf(codes.Internal, "failed to parse server ID from path: %v", err) + } + return serverID.String(), nil +} + +// getPublicKeyFromCryptoKeyVersion requests Cloud KMS to get the public key +// of the specified CryptoKeyVersion. +func getPublicKeyFromCryptoKeyVersion(ctx context.Context, log hclog.Logger, kmsClient cloudKeyManagementService, cryptoKeyVersionName string) ([]byte, error) { + kmsPublicKey, errGetPublicKey := kmsClient.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: cryptoKeyVersionName}) + attempts := 1 + + log = log.With(cryptoKeyVersionNameTag, cryptoKeyVersionName) + for errGetPublicKey != nil { + if attempts > getPublicKeyMaxAttempts { + log.Error("Could not get the public key because the CryptoKeyVersion is still being generated. Maximum number of attempts reached.") + return nil, errGetPublicKey + } + cryptoKeyVersion, errGetCryptoKeyVersion := kmsClient.GetCryptoKeyVersion(ctx, &kmspb.GetCryptoKeyVersionRequest{ + Name: cryptoKeyVersionName, + }) + if errGetCryptoKeyVersion != nil { + return nil, errGetCryptoKeyVersion + } + + // Check if the CryptoKeyVersion is still being generated or + // if it is now enabled. + // Longer generation times can be observed when using algorithms + // with large key sizes. (e.g. when rsa-4096 keys are used). + // One or two additional attempts is usually enough to find the + // CryptoKeyVersion enabled. + switch cryptoKeyVersion.State { + case kmspb.CryptoKeyVersion_PENDING_GENERATION: + // This is a recoverable error. + case kmspb.CryptoKeyVersion_ENABLED: + // The CryptoKeyVersion may be ready to be used now. + default: + // We cannot recover if it's in a different status. + return nil, errGetPublicKey + } + + log.Warn("Could not get the public key because the CryptoKeyVersion is still being generated. Trying again.") + attempts++ + kmsPublicKey, errGetPublicKey = kmsClient.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: cryptoKeyVersionName}) + } + + // Perform integrity verification. + if int64(crc32Checksum([]byte(kmsPublicKey.Pem))) != kmsPublicKey.PemCrc32C.Value { + return nil, fmt.Errorf("response corrupted in-transit") + } + + pemBlock, _ := pem.Decode([]byte(kmsPublicKey.Pem)) + return pemBlock.Bytes, nil +} + +func makeFingerprint(pkixData []byte) string { + s := sha256.Sum256(pkixData) + return hex.EncodeToString(s[:]) +} + +// min returns the minimum of the provided time durations. +func min(x, y time.Duration) time.Duration { + if x < y { + return x + } + return y +} + +// parseAndValidateConfig returns an error if any configuration provided does +// not meet acceptable criteria +func parseAndValidateConfig(c string) (*Config, error) { + config := new(Config) + + if err := hcl.Decode(config, c); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "unable to decode configuration: %v", err) + } + + if config.KeyRing == "" { + return nil, status.Error(codes.InvalidArgument, "configuration is missing the key ring") + } + + if config.KeyMetadataFile == "" { + return nil, status.Error(codes.InvalidArgument, "configuration is missing server ID file path") + } + + return config, nil +} + +// parsePolicyFile parses a file containing iam.Policy3 data in JSON format. +func parsePolicyFile(policyFile string) (*iam.Policy3, error) { + policyBytes, err := os.ReadFile(policyFile) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + policy := &iam.Policy3{} + if err := json.Unmarshal(policyBytes, policy); err != nil { + return nil, fmt.Errorf("failed to parse custom JSON policy: %w", err) + } + + return policy, nil +} diff --git a/pkg/server/plugin/keymanager/gcpkms/gcpkms_test.go b/pkg/server/plugin/keymanager/gcpkms/gcpkms_test.go new file mode 100644 index 0000000000..7f9dfd4335 --- /dev/null +++ b/pkg/server/plugin/keymanager/gcpkms/gcpkms_test.go @@ -0,0 +1,1750 @@ +package gcpkms + +import ( + "context" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "testing" + "time" + + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spiffe/go-spiffe/v2/spiffeid" + keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1" + configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" + "github.com/spiffe/spire/pkg/common/catalog" + "github.com/spiffe/spire/pkg/server/plugin/keymanager" + keymanagertest "github.com/spiffe/spire/pkg/server/plugin/keymanager/test" + "github.com/spiffe/spire/test/clock" + "github.com/spiffe/spire/test/plugintest" + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +const ( + customPolicy = ` +{ + "bindings": [ + { + "role": "projects/test-project/roles/role-name", + "members": [ + "serviceAccount:test-sa@example.com" + ] + } + ], + "version": 3 +} +` + pemCert = `-----BEGIN CERTIFICATE----- +MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa +GA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyv +sCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXs +RxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkw +F4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09X +makw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylA +dZglS5kKnYigmwDh+/U= +-----END CERTIFICATE----- +` + spireKeyID1 = "spireKeyID-1" + spireKeyID2 = "spireKeyID-2" + testTimeout = 60 * time.Second + validPolicyFile = "custom_policy_file.json" + validServerID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + validServerIDFile = "test-server-id" + validKeyRing = "projects/project-name/locations/location-name/keyRings/key-ring-name" +) + +var ( + ctx = context.Background() + cryptoKeyName1 = path.Join(validKeyRing, "cryptoKeys", fmt.Sprintf("test-crypto-key/spire-key-%s-spireKeyID-1", validServerID)) + cryptoKeyName2 = path.Join(validKeyRing, "cryptoKeys", fmt.Sprintf("test-crypto-key/spire-key-%s-spireKeyID-2", validServerID)) + fakeTime = timestamppb.Now() + unixEpoch = time.Unix(0, 0) + + pubKey = &kmspb.PublicKey{ + Pem: pemCert, + PemCrc32C: &wrapperspb.Int64Value{Value: int64(crc32Checksum([]byte(pemCert)))}, + } +) + +type pluginTest struct { + plugin *Plugin + fakeKMSClient *fakeKMSClient + log logrus.FieldLogger + logHook *test.Hook + clockHook *clock.Mock +} + +func setupTest(t *testing.T) *pluginTest { + log, logHook := test.NewNullLogger() + log.Level = logrus.DebugLevel + + c := clock.NewMock(t) + c.Set(unixEpoch) + fakeKMSClient := newKMSClientFake(t, c) + p := newPlugin( + func(ctx context.Context, opts ...option.ClientOption) (cloudKeyManagementService, error) { + fakeKMSClient.opts = opts + return fakeKMSClient, nil + }, + ) + km := new(keymanager.V1) + plugintest.Load(t, builtin(p), km, plugintest.Log(log)) + + p.hooks.clk = c + + return &pluginTest{ + plugin: p, + fakeKMSClient: fakeKMSClient, + log: log, + logHook: logHook, + clockHook: c, + } +} + +func TestConfigure(t *testing.T) { + for _, tt := range []struct { + name string + expectMsg string + expectCode codes.Code + expectOpts []option.ClientOption + config *Config + configureRequest *configv1.ConfigureRequest + fakeCryptoKeys []*fakeCryptoKey + getCryptoKeyVersionErr error + listCryptoKeysErr error + describeKeyErr error + getPublicKeyErr error + getPublicKeyErrCount int + }{ + { + name: "pass with keys", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyRing: validKeyRing, + }, + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "2": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/2", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName2, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName2), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName2, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "2": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/2", cryptoKeyName2), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + }, + }, + { + name: "pass without keys", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyRing: validKeyRing, + }, + }, + { + name: "pass without keys - using a service account file", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyRing: validKeyRing, + ServiceAccountFile: "service-account-file", + }, + expectOpts: []option.ClientOption{option.WithCredentialsFile("service-account-file")}, + }, + { + name: "missing key ring", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + }, + expectMsg: "configuration is missing the key ring", + expectCode: codes.InvalidArgument, + }, + { + name: "missing key metadata file", + config: &Config{ + KeyRing: validKeyRing, + }, + expectMsg: "configuration is missing server ID file path", + expectCode: codes.InvalidArgument, + }, + { + name: "custom policy file does not exist", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyPolicyFile: "non-existent-file.json", + KeyRing: validKeyRing, + }, + expectMsg: fmt.Sprintf("could not parse policy file: failed to read file: open non-existent-file.json: %s", spiretest.FileNotFound()), + expectCode: codes.Internal, + }, + { + name: "use custom policy file", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyPolicyFile: getCustomPolicyFile(t), + KeyRing: validKeyRing, + }, + }, + { + name: "empty key metadata file", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, ""), + KeyRing: validKeyRing, + }, + }, + { + name: "invalid server ID in metadata file", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, "invalid-id"), + KeyRing: validKeyRing, + }, + expectMsg: "failed to parse server ID from path: uuid: incorrect UUID length 10 in string \"invalid-id\"", + expectCode: codes.Internal, + }, + { + name: "invalid metadata file path", + config: &Config{ + KeyMetadataFile: "/", + KeyRing: validKeyRing, + }, + expectMsg: "failed to read server ID from path: read /:", + expectCode: codes.Internal, + }, + { + name: "decode error", + configureRequest: configureRequestWithString("{ malformed json }"), + expectMsg: "unable to decode configuration: 1:11: illegal char", + expectCode: codes.InvalidArgument, + }, + { + name: "ListCryptoKeys error", + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyRing: validKeyRing, + }, + expectMsg: "failed to list SPIRE Server keys in Cloud KMS: error listing CryptoKeys", + expectCode: codes.Internal, + listCryptoKeysErr: errors.New("error listing CryptoKeys"), + }, + { + name: "unsupported CryptoKeyVersionAlgorithm", + expectMsg: "failed to fetch entries: unsupported CryptoKeyVersionAlgorithm: GOOGLE_SYMMETRIC_ENCRYPTION", + expectCode: codes.Internal, + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyRing: validKeyRing, + }, + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_GOOGLE_SYMMETRIC_ENCRYPTION}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: &kmspb.PublicKey{}, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_GOOGLE_SYMMETRIC_ENCRYPTION, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + }, + }, + { + name: "get public key error max attempts", + expectMsg: "failed to fetch entries: error getting public key: get public key error", + expectCode: codes.Internal, + config: &Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyRing: validKeyRing, + }, + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + }, + getPublicKeyErr: errors.New("get public key error"), + getPublicKeyErrCount: getPublicKeyMaxAttempts + 1, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := setupTest(t) + ts.fakeKMSClient.putFakeCryptoKeys(tt.fakeCryptoKeys) + ts.fakeKMSClient.setListCryptoKeysErr(tt.listCryptoKeysErr) + ts.fakeKMSClient.setGetCryptoKeyVersionErr(tt.getCryptoKeyVersionErr) + ts.fakeKMSClient.setGetPublicKeySequentialErrs(tt.getPublicKeyErr, tt.getPublicKeyErrCount) + + var configureRequest *configv1.ConfigureRequest + if tt.config != nil { + require.Nil(t, tt.configureRequest, "The test case must define a configuration or a configuration request, not both.") + configureRequest = configureRequestFromConfig(tt.config) + } else { + require.Nil(t, tt.config, "The test case must define a configuration or a configuration request, not both.") + configureRequest = tt.configureRequest + } + _, err := ts.plugin.Configure(ctx, configureRequest) + + spiretest.RequireGRPCStatusContains(t, err, tt.expectCode, tt.expectMsg) + if tt.expectCode != codes.OK { + return + } + require.NoError(t, err) + + // Assert the config settings + require.Equal(t, tt.config, ts.plugin.config) + + // Assert that the keys have been loaded + storedFakeCryptoKeys := ts.fakeKMSClient.store.fetchFakeCryptoKeys() + for _, expectedFakeCryptoKey := range storedFakeCryptoKeys { + spireKeyID, ok := getSPIREKeyIDFromCryptoKeyName(expectedFakeCryptoKey.Name) + require.True(t, ok) + + entry, ok := ts.plugin.entries[spireKeyID] + require.True(t, ok) + require.Equal(t, expectedFakeCryptoKey.CryptoKey, entry.cryptoKey) + } + + require.Equal(t, tt.expectOpts, ts.plugin.kmsClient.(*fakeKMSClient).opts) + }) + } +} + +func TestDisposeStaleCryptoKeys(t *testing.T) { + configureRequest := configureRequestWithDefaults(t) + fakeCryptoKeys := []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName2, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName2), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + } + + ts := setupTest(t) + ts.fakeKMSClient.putFakeCryptoKeys(fakeCryptoKeys) + + ts.plugin.hooks.disposeCryptoKeysSignal = make(chan error) + ts.plugin.hooks.scheduleDestroySignal = make(chan error) + ts.plugin.hooks.setInactiveSignal = make(chan error) + + _, err := ts.plugin.Configure(ctx, configureRequest) + require.NoError(t, err) + + // Move the clock to start disposeCryptoKeysTask. + ts.clockHook.Add(disposeCryptoKeysFrequency) + + // Wait for dispose disposeCryptoKeysTask to be initialized. + _ = waitForSignal(t, ts.plugin.hooks.disposeCryptoKeysSignal) + + // Move the clock to make sure that we have stale CryptoKeys. + ts.clockHook.Add(maxStaleDuration) + + // Wait for destroy notification of all the CryptoKeyVersions. + storedFakeCryptoKeys := ts.fakeKMSClient.store.fetchFakeCryptoKeys() + for _, fakeKey := range storedFakeCryptoKeys { + storedFakeCryptoKeyVersions := fakeKey.fetchFakeCryptoKeyVersions() + for range storedFakeCryptoKeyVersions { + _ = waitForSignal(t, ts.plugin.hooks.scheduleDestroySignal) + } + } + + for _, fakeKey := range storedFakeCryptoKeys { + // The CryptoKeys should be active until the next run of disposeCryptoKeys. + require.Equal(t, "true", fakeKey.getLabelValue(labelNameActive)) + + storedFakeCryptoKeyVersions := fakeKey.fetchFakeCryptoKeyVersions() + for _, fakeKeyVersion := range storedFakeCryptoKeyVersions { + // The status should be changed to CryptoKeyVersion_DESTROY_SCHEDULED. + require.Equal(t, kmspb.CryptoKeyVersion_DESTROY_SCHEDULED, fakeKeyVersion.State, fmt.Sprintf("state mismatch in CryptokeyVersion %q", fakeKeyVersion.Name)) + } + } + + // Move the clock to start disposeCryptoKeysTask again. + ts.clockHook.Add(disposeCryptoKeysFrequency) + + // Wait for dispose disposeCryptoKeysTask to be initialized. + _ = waitForSignal(t, ts.plugin.hooks.disposeCryptoKeysSignal) + + // Since the CryptoKey doesn't have any enabled CryptoKeyVersions at + // this point, it should be set as inactive. + // Wait for the set inactive signal. + // The order is not respected, so verify no error is returned + // and that all signals received + for _, fakeKey := range storedFakeCryptoKeys { + err = waitForSignal(t, ts.plugin.hooks.setInactiveSignal) + require.NoErrorf(t, err, "unexpected error on %v", fakeKey.getName()) + } + + for _, fakeKey := range storedFakeCryptoKeys { + // The CryptoKey should be inactive now. + fakeKey, ok := ts.fakeKMSClient.store.fetchFakeCryptoKey(fakeKey.getName()) + require.True(t, ok) + require.Equal(t, "false", fakeKey.getLabelValue(labelNameActive)) + } +} + +func TestDisposeActiveCryptoKeys(t *testing.T) { + configureRequest := configureRequestWithDefaults(t) + fakeCryptoKeys := []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName2, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName2), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + } + + ts := setupTest(t) + ts.fakeKMSClient.putFakeCryptoKeys(fakeCryptoKeys) + + ts.plugin.hooks.disposeCryptoKeysSignal = make(chan error) + scheduleDestroySignal := make(chan error) + ts.plugin.hooks.scheduleDestroySignal = scheduleDestroySignal + + _, err := ts.plugin.Configure(ctx, configureRequest) + require.NoError(t, err) + + // Move the clock to start disposeCryptoKeysTask. + ts.clockHook.Add(disposeCryptoKeysFrequency) + + // Wait for dispose disposeCryptoKeysTask to be initialized. + _ = waitForSignal(t, ts.plugin.hooks.disposeCryptoKeysSignal) + + // The CryptoKeys are not stale yet. Assert that they are active and the + // CryptoKeyVersions enabled. + storedFakeCryptoKeys := ts.fakeKMSClient.store.fetchFakeCryptoKeys() + for _, fakeKey := range storedFakeCryptoKeys { + require.Equal(t, "true", fakeKey.getLabelValue(labelNameActive)) + storedFakeCryptoKeyVersions := fakeKey.fetchFakeCryptoKeyVersions() + for _, fakeKeyVersion := range storedFakeCryptoKeyVersions { + require.Equal(t, kmspb.CryptoKeyVersion_ENABLED, fakeKeyVersion.GetState(), fakeKeyVersion.GetName()) + } + } +} + +func TestEnqueueDestructionFailure(t *testing.T) { + configureRequest := configureRequestWithDefaults(t) + fakeCryptoKeys := []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName2, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName2), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + } + + ts := setupTest(t) + // Change the scheduleDestroy channel to be unbuffered. + ts.plugin.scheduleDestroy = make(chan string) + + ts.fakeKMSClient.putFakeCryptoKeys(fakeCryptoKeys) + + ts.plugin.hooks.disposeCryptoKeysSignal = make(chan error, 1) + ts.plugin.hooks.enqueueDestructionSignal = make(chan error, 1) + + _, err := ts.plugin.Configure(ctx, configureRequest) + require.NoError(t, err) + + // Move the clock to start disposeCryptoKeysTask. + ts.clockHook.Add(disposeCryptoKeysFrequency) + + // Wait for dispose disposeCryptoKeysTask to be initialized. + _ = waitForSignal(t, ts.plugin.hooks.disposeCryptoKeysSignal) + + // Move the clock to make sure that we have stale CryptoKeys. + ts.clockHook.Add(maxStaleDuration) + + // Enqueuing the first CryptoKeyVersion for destruction should succeed. + err = waitForSignal(t, ts.plugin.hooks.enqueueDestructionSignal) + require.NoError(t, err) + + // Enqueuing the second CryptoKeyVersion for destruction should fail. + err = waitForSignal(t, ts.plugin.hooks.enqueueDestructionSignal) + require.ErrorContains(t, err, "could not enqueue CryptoKeyVersion") +} + +func TestGenerateKey(t *testing.T) { + for _, tt := range []struct { + configureReq *configv1.ConfigureRequest + expectCode codes.Code + expectMsg string + destroyTime *timestamp.Timestamp + fakeCryptoKeys []*fakeCryptoKey + generateKeyReq *keymanagerv1.GenerateKeyRequest + logs []spiretest.LogEntry + name string + testDisabled bool + waitForDelete bool + initialCryptoKeyVersionState kmspb.CryptoKeyVersion_CryptoKeyVersionState + + createKeyErr error + destroyCryptoKeyVersionErr error + getCryptoKeyVersionErr error + getPublicKeyErr error + getPublicKeyErrCount int + getTokenInfoErr error + updateCryptoKeyErr error + }{ + { + name: "success: non existing key", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + }, + { + name: "success: keeps retrying when crypto key is in pending generation state", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + initialCryptoKeyVersionState: kmspb.CryptoKeyVersion_PENDING_GENERATION, + getPublicKeyErr: errors.New("error getting public key"), + getPublicKeyErrCount: 5, + }, + { + name: "success: non existing key with special characters", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: "bundle-acme-foo.bar+rsa", + KeyType: keymanagerv1.KeyType_EC_P256, + }, + }, + { + name: "success: non existing key with default policy", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + configureReq: configureRequestWithVars(createKeyMetadataFile(t, ""), "", validKeyRing, "service_account_file"), + }, + { + name: "success: non existing key with custom policy", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + configureReq: configureRequestWithVars(createKeyMetadataFile(t, ""), getCustomPolicyFile(t), validKeyRing, "service_account_file"), + }, + { + name: "success: replace old key", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + }, + waitForDelete: true, + destroyTime: fakeTime, + logs: []spiretest.LogEntry{ + { + Level: logrus.DebugLevel, + Message: "CryptoKeyVersion scheduled for destruction", + Data: logrus.Fields{ + cryptoKeyVersionNameTag: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + scheduledDestroyTimeTag: fakeTime.AsTime().String(), + }, + }, + }, + }, + { + name: "success: EC 384", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P384, + }, + }, + { + name: "success: RSA 2048", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_RSA_2048, + }, + }, + { + name: "success: RSA 4096", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_RSA_4096, + }, + }, + { + name: "missing key id", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: "", + KeyType: keymanagerv1.KeyType_EC_P256, + }, + expectMsg: "key id is required", + expectCode: codes.InvalidArgument, + }, + { + name: "missing key type", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_UNSPECIFIED_KEY_TYPE, + }, + expectMsg: "key type is required", + expectCode: codes.InvalidArgument, + }, + { + name: "unsupported key type", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: 100, + }, + expectMsg: "failed to generate key: unsupported key type \"100\"", + expectCode: codes.Internal, + }, + { + name: "create CryptoKey error", + expectMsg: "failed to create CryptoKey: error creating CryptoKey", + expectCode: codes.Internal, + createKeyErr: errors.New("error creating CryptoKey"), + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + }, + { + name: "get public key error", + expectMsg: "failed to get public key: public key error", + expectCode: codes.Internal, + getPublicKeyErr: errors.New("public key error"), + getPublicKeyErrCount: 1, + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + }, + { + name: "cryptoKeyVersion not found when scheduling for destruction", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + destroyCryptoKeyVersionErr: status.Error(codes.NotFound, ""), + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + }, + waitForDelete: true, + destroyTime: fakeTime, + logs: []spiretest.LogEntry{ + { + Level: logrus.WarnLevel, + Message: "CryptoKeyVersion not found", + Data: logrus.Fields{ + cryptoKeyVersionNameTag: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + }, + }, + }, + }, + { + name: "schedule destroy error", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + destroyCryptoKeyVersionErr: errors.New("error scheduling CryptoKeyVersion for destruction"), + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + }, + waitForDelete: true, + destroyTime: fakeTime, + logs: []spiretest.LogEntry{ + { + Level: logrus.ErrorLevel, + Message: "It was not possible to schedule CryptoKeyVersion for destruction", + Data: logrus.Fields{ + cryptoKeyVersionNameTag: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + reasonTag: "error scheduling CryptoKeyVersion for destruction", + }, + }, + }, + }, + { + name: "cryptoKeyVersion to destroy not enabled", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + destroyCryptoKeyVersionErr: errors.New("error scheduling CryptoKeyVersion for destruction"), + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + }, + testDisabled: true, + waitForDelete: true, + destroyTime: fakeTime, + logs: []spiretest.LogEntry{ + { + Level: logrus.WarnLevel, + Message: "CryptoKeyVersion is not enabled, will not be scheduled for destruction", + Data: logrus.Fields{ + cryptoKeyVersionNameTag: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + cryptoKeyVersionStateTag: kmspb.CryptoKeyVersion_DISABLED.String(), + }, + }, + }, + }, + { + name: "error getting CryptoKeyVersion", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + destroyCryptoKeyVersionErr: errors.New("error scheduling CryptoKeyVersion for destruction"), + getCryptoKeyVersionErr: errors.New("error getting CryptoKeyVersion"), + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }, + }, + }, + }, + }, + waitForDelete: true, + destroyTime: fakeTime, + logs: []spiretest.LogEntry{ + { + Level: logrus.ErrorLevel, + Message: "Could not get the CryptoKeyVersion while trying to schedule it for destruction", + Data: logrus.Fields{ + cryptoKeyVersionNameTag: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + reasonTag: "error getting CryptoKeyVersion", + }, + }, + }, + }, + { + name: "error getting token info", + expectCode: codes.Internal, + expectMsg: "could not get token information: error getting token info", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P384, + }, + getTokenInfoErr: errors.New("error getting token info"), + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := setupTest(t) + ts.fakeKMSClient.setDestroyTime(fakeTime) + ts.fakeKMSClient.putFakeCryptoKeys(tt.fakeCryptoKeys) + ts.fakeKMSClient.setCreateCryptoKeyErr(tt.createKeyErr) + ts.fakeKMSClient.setInitialCryptoKeyVersionState(tt.initialCryptoKeyVersionState) + ts.fakeKMSClient.setGetCryptoKeyVersionErr(tt.getCryptoKeyVersionErr) + ts.fakeKMSClient.setGetTokeninfoErr(tt.getTokenInfoErr) + ts.fakeKMSClient.setUpdateCryptoKeyErr(tt.updateCryptoKeyErr) + ts.fakeKMSClient.setDestroyCryptoKeyVersionErr(tt.destroyCryptoKeyVersionErr) + ts.fakeKMSClient.setIsKeyDisabled(tt.testDisabled) + + ts.plugin.hooks.scheduleDestroySignal = make(chan error) + + configureReq := tt.configureReq + if configureReq == nil { + configureReq = configureRequestWithDefaults(t) + } + + coreConfig := catalog.CoreConfig{ + TrustDomain: spiffeid.RequireTrustDomainFromString("test.example.org"), + } + km := new(keymanager.V1) + var err error + + plugintest.Load(t, builtin(ts.plugin), km, + plugintest.CaptureConfigureError(&err), + plugintest.CoreConfig(coreConfig), + plugintest.Configure(configureReq.HclConfiguration), + plugintest.Log(ts.log), + ) + require.NoError(t, err) + + ts.fakeKMSClient.setGetPublicKeySequentialErrs(tt.getPublicKeyErr, tt.getPublicKeyErrCount) + + resp, err := ts.plugin.GenerateKey(ctx, tt.generateKeyReq) + if tt.expectMsg != "" { + spiretest.RequireGRPCStatusContains(t, err, tt.expectCode, tt.expectMsg) + return + } + + require.NoError(t, err) + require.NotNil(t, resp) + + _, err = ts.plugin.GetPublicKey(ctx, &keymanagerv1.GetPublicKeyRequest{ + KeyId: tt.generateKeyReq.KeyId, + }) + require.NoError(t, err) + + if !tt.waitForDelete { + spiretest.AssertLogsContainEntries(t, ts.logHook.AllEntries(), tt.logs) + return + } + + select { + case <-ts.plugin.hooks.scheduleDestroySignal: + // The logs emitted by the deletion goroutine and those that + // enqueue deletion can be intermixed, so we cannot depend + // on the exact order of the logs, so we just assert that + // the expected log lines are present somewhere. + spiretest.AssertLogsContainEntries(t, ts.logHook.AllEntries(), tt.logs) + case <-time.After(testTimeout): + t.Fail() + } + }) + } +} + +func TestKeepActiveCryptoKeys(t *testing.T) { + for _, tt := range []struct { + configureRequest *configv1.ConfigureRequest + expectError string + fakeCryptoKeys []*fakeCryptoKey + name string + updateCryptoKeyErr error + }{ + { + name: "keep active CryptoKeys error", + configureRequest: configureRequestWithDefaults(t), + expectError: "error updating CryptoKey", + updateCryptoKeyErr: errors.New("error updating CryptoKey"), + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + }, + }, + { + name: "keep active CryptoKeys succeeds", + configureRequest: configureRequestWithDefaults(t), + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName2, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName2), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + }, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := setupTest(t) + ts.fakeKMSClient.putFakeCryptoKeys(tt.fakeCryptoKeys) + ts.fakeKMSClient.setUpdateCryptoKeyErr(tt.updateCryptoKeyErr) + ts.plugin.hooks.keepActiveCryptoKeysSignal = make(chan error) + + _, err := ts.plugin.Configure(ctx, tt.configureRequest) + require.NoError(t, err) + + // Wait for keepActiveCryptoKeys task to be initialized. + _ = waitForSignal(t, ts.plugin.hooks.keepActiveCryptoKeysSignal) + + // Move the clock forward so the task is run. + currentTime := unixEpoch.Add(6 * time.Hour) + ts.clockHook.Set(currentTime) + + // Wait for keepActiveCryptoKeys to be run. + err = waitForSignal(t, ts.plugin.hooks.keepActiveCryptoKeysSignal) + + if tt.updateCryptoKeyErr != nil { + require.NotNil(t, err) + require.EqualError(t, err, err.Error()) + return + } + require.NoError(t, err) + + storedFakeCryptoKeys := ts.fakeKMSClient.store.fetchFakeCryptoKeys() + for _, fakeKey := range storedFakeCryptoKeys { + require.EqualValues(t, fakeKey.getLabelValue(labelNameLastUpdate), fmt.Sprint(currentTime.Unix()), fakeKey.CryptoKey.Name) + } + }) + } +} + +func TestGetPublicKeys(t *testing.T) { + for _, tt := range []struct { + name string + err string + fakeCryptoKeys []*fakeCryptoKey + }{ + { + name: "one key", + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + }, + }, + { + name: "multiple keys", + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName2, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName2), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + }, + }, + { + name: "non existing keys", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := setupTest(t) + ts.fakeKMSClient.putFakeCryptoKeys(tt.fakeCryptoKeys) + _, err := ts.plugin.Configure(ctx, configureRequestWithDefaults(t)) + require.NoError(t, err) + + resp, err := ts.plugin.GetPublicKeys(ctx, &keymanagerv1.GetPublicKeysRequest{}) + + if tt.err != "" { + require.Error(t, err) + require.EqualError(t, err, tt.err) + return + } + + require.NotNil(t, resp) + require.NoError(t, err) + storedFakeCryptoKeys := ts.fakeKMSClient.store.fetchFakeCryptoKeys() + for _, fakeKey := range storedFakeCryptoKeys { + storedFakeCryptoKeyVersions := fakeKey.fetchFakeCryptoKeyVersions() + for _, fakeKeyVersion := range storedFakeCryptoKeyVersions { + pubKey, err := getPublicKeyFromCryptoKeyVersion(ctx, ts.plugin.log, ts.fakeKMSClient, fakeKeyVersion.CryptoKeyVersion.Name) + require.NoError(t, err) + require.Equal(t, pubKey, resp.PublicKeys[0].PkixData) + } + } + }) + } +} + +func TestGetPublicKey(t *testing.T) { + for _, tt := range []struct { + name string + expectCodeConfigure codes.Code + expectMsgConfigure string + expectCodeGetPublicKey codes.Code + expectMsgGetPublicKey string + fakeCryptoKeys []*fakeCryptoKey + keyID string + pemCrc32C *wrapperspb.Int64Value + }{ + { + name: "existing key", + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + }, + keyID: spireKeyID1, + }, + { + name: "integrity verification error", + expectCodeConfigure: codes.Internal, + expectMsgConfigure: "failed to fetch entries: error getting public key: response corrupted in-transit", + fakeCryptoKeys: []*fakeCryptoKey{ + { + CryptoKey: &kmspb.CryptoKey{ + Name: cryptoKeyName1, + Labels: map[string]string{labelNameActive: "true"}, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256}, + }, + fakeCryptoKeyVersions: map[string]*fakeCryptoKeyVersion{ + "1": { + publicKey: pubKey, + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + Name: fmt.Sprintf("%s/cryptoKeyVersions/1", cryptoKeyName1), + State: kmspb.CryptoKeyVersion_ENABLED, + }}, + }, + }, + }, + keyID: spireKeyID1, + pemCrc32C: &wrapperspb.Int64Value{Value: 1}, + }, + { + name: "non existing key", + expectMsgGetPublicKey: fmt.Sprintf("key %q not found", spireKeyID1), + expectCodeGetPublicKey: codes.NotFound, + keyID: spireKeyID1, + }, + { + name: "missing key id", + expectMsgGetPublicKey: "key id is required", + expectCodeGetPublicKey: codes.InvalidArgument, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := setupTest(t) + ts.fakeKMSClient.setPEMCrc32C(tt.pemCrc32C) + ts.fakeKMSClient.putFakeCryptoKeys(tt.fakeCryptoKeys) + + _, err := ts.plugin.Configure(ctx, configureRequestWithDefaults(t)) + if tt.expectMsgConfigure != "" { + spiretest.RequireGRPCStatusContains(t, err, tt.expectCodeConfigure, tt.expectMsgConfigure) + return + } + + require.NoError(t, err) + resp, err := ts.plugin.GetPublicKey(ctx, &keymanagerv1.GetPublicKeyRequest{ + KeyId: tt.keyID, + }) + if tt.expectMsgGetPublicKey != "" { + spiretest.RequireGRPCStatusContains(t, err, tt.expectCodeGetPublicKey, tt.expectMsgGetPublicKey) + return + } + require.NotNil(t, resp) + require.NoError(t, err) + require.Equal(t, tt.keyID, resp.PublicKey.Id) + require.Equal(t, ts.plugin.entries[tt.keyID].publicKey, resp.PublicKey) + }) + } +} + +func TestKeyManagerContract(t *testing.T) { + create := func(t *testing.T) keymanager.KeyManager { + dir := t.TempDir() + c := clock.NewMock(t) + fakeKMSClient := newKMSClientFake(t, c) + p := newPlugin( + func(ctx context.Context, opts ...option.ClientOption) (cloudKeyManagementService, error) { + return fakeKMSClient, nil + }, + ) + km := new(keymanager.V1) + keyMetadataFile := filepath.ToSlash(filepath.Join(dir, "metadata.json")) + plugintest.Load(t, builtin(p), km, plugintest.Configuref(` + key_metadata_file = %q + key_ring = "projects/project-id/locations/location/keyRings/keyring" + `, keyMetadataFile)) + return km + } + + unsupportedSignatureAlgorithms := map[keymanager.KeyType][]x509.SignatureAlgorithm{ + keymanager.ECP256: {x509.ECDSAWithSHA384, x509.ECDSAWithSHA512}, + keymanager.ECP384: {x509.ECDSAWithSHA256, x509.ECDSAWithSHA512}, + keymanager.RSA2048: {x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS, x509.SHA384WithRSA, x509.SHA512WithRSA}, + keymanager.RSA4096: {x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS, x509.SHA384WithRSA, x509.SHA512WithRSA}, + } + keymanagertest.Test(t, keymanagertest.Config{ + Create: create, + UnsupportedSignatureAlgorithms: unsupportedSignatureAlgorithms, + }) +} + +func TestSetIAMPolicy(t *testing.T) { + for _, tt := range []struct { + name string + policyErr error + setPolicyErr error + expectError string + useCustomPolicy bool + }{ + { + name: "set default policy", + }, + { + name: "set default policy - error", + expectError: "failed to set default IAM policy: error setting default policy", + setPolicyErr: errors.New("error setting default policy"), + }, + { + name: "set custom policy", + useCustomPolicy: true, + }, + { + name: "set custom policy - error", + expectError: "failed to set custom IAM policy: error setting custom policy", + setPolicyErr: errors.New("error setting custom policy"), + useCustomPolicy: true, + }, + { + name: "get policy error", + expectError: "failed to retrieve IAM policy: error getting policy", + policyErr: errors.New("error getting policy"), + useCustomPolicy: true, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := setupTest(t) + ts.fakeKMSClient.fakeIAMHandle.setPolicyError(tt.policyErr) + ts.fakeKMSClient.fakeIAMHandle.setSetPolicyErr(tt.setPolicyErr) + + var configureReq *configv1.ConfigureRequest + if tt.useCustomPolicy { + customPolicyFile := getCustomPolicyFile(t) + configureReq = configureRequestFromConfig(&Config{ + KeyMetadataFile: createKeyMetadataFile(t, validServerID), + KeyPolicyFile: customPolicyFile, + KeyRing: validKeyRing, + ServiceAccountFile: "service_account_file", + }) + expectedPolicy, err := parsePolicyFile(customPolicyFile) + require.NoError(t, err) + ts.fakeKMSClient.fakeIAMHandle.setExpectedPolicy(expectedPolicy) + } else { + ts.fakeKMSClient.fakeIAMHandle.setExpectedPolicy(ts.fakeKMSClient.getDefaultPolicy()) + configureReq = configureRequestWithDefaults(t) + } + _, err := ts.plugin.Configure(ctx, configureReq) + require.NoError(t, err) + + err = ts.plugin.setIamPolicy(ctx, cryptoKeyName1) + if tt.expectError != "" { + require.EqualError(t, err, tt.expectError) + return + } + require.NoError(t, err) + }) + } +} + +func TestSignData(t *testing.T) { + sum256 := sha256.Sum256(nil) + sum384 := sha512.Sum384(nil) + + for _, tt := range []struct { + name string + asymmetricSignErr error + expectMsg string + expectCode codes.Code + generateKeyReq *keymanagerv1.GenerateKeyRequest + signDataReq *keymanagerv1.SignDataRequest + signatureCrc32C *wrapperspb.Int64Value + }{ + { + name: "pass EC SHA256", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + }, + { + name: "pass EC SHA384", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P384, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum384[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA384, + }, + }, + }, + { + name: "pass RSA 2048 SHA 256", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_RSA_2048, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + }, + { + name: "pass RSA 4096 SHA 256", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_RSA_4096, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + }, + { + name: "pass RSA 2048 SHA 256", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_RSA_2048, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + }, + { + name: "missing key id", + expectCode: codes.InvalidArgument, + expectMsg: "key id is required", + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: "", + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + }, + { + name: "missing key signer opts", + expectCode: codes.InvalidArgument, + expectMsg: "signer opts is required", + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + }, + }, + { + name: "missing hash algorithm", + expectCode: codes.InvalidArgument, + expectMsg: "hash algorithm is required", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_UNSPECIFIED_HASH_ALGORITHM, + }, + }, + }, + { + name: "usupported hash algorithm", + expectCode: codes.InvalidArgument, + expectMsg: "hash algorithm not supported", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: 100, + }, + }, + }, + { + name: "non existing key", + expectCode: codes.NotFound, + expectMsg: "key \"does_not_exists\" not found", + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: "does_not_exists", + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + }, + { + name: "pss not supported", + expectCode: codes.InvalidArgument, + expectMsg: "the only RSA signature scheme supported is RSASSA-PKCS1-v1_5", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_RSA_2048, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_PssOptions{ + PssOptions: &keymanagerv1.SignDataRequest_PSSOptions{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + SaltLength: 256, + }, + }, + }, + }, + { + name: "sign error", + asymmetricSignErr: errors.New("error signing"), + expectCode: codes.Internal, + expectMsg: "failed to sign: error signing", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + }, + { + name: "integrity verification error", + expectCode: codes.Internal, + expectMsg: "error signing: response corrupted in-transit", + generateKeyReq: &keymanagerv1.GenerateKeyRequest{ + KeyId: spireKeyID1, + KeyType: keymanagerv1.KeyType_EC_P256, + }, + signDataReq: &keymanagerv1.SignDataRequest{ + KeyId: spireKeyID1, + Data: sum256[:], + SignerOpts: &keymanagerv1.SignDataRequest_HashAlgorithm{ + HashAlgorithm: keymanagerv1.HashAlgorithm_SHA256, + }, + }, + signatureCrc32C: &wrapperspb.Int64Value{Value: 1}, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := setupTest(t) + ts.fakeKMSClient.setAsymmetricSignErr(tt.asymmetricSignErr) + ts.fakeKMSClient.setSignatureCrc32C(tt.signatureCrc32C) + _, err := ts.plugin.Configure(ctx, configureRequestWithDefaults(t)) + require.NoError(t, err) + if tt.generateKeyReq != nil { + _, err := ts.plugin.GenerateKey(ctx, tt.generateKeyReq) + require.NoError(t, err) + } + + resp, err := ts.plugin.SignData(ctx, tt.signDataReq) + spiretest.RequireGRPCStatusContains(t, err, tt.expectCode, tt.expectMsg) + if tt.expectCode != codes.OK { + return + } + require.NotNil(t, resp) + }) + } +} + +func configureRequestFromConfig(c *Config) *configv1.ConfigureRequest { + return &configv1.ConfigureRequest{ + HclConfiguration: fmt.Sprintf(`{ + "key_metadata_file":"%s", + "key_policy_file":"%s", + "key_ring":"%s", + "service_account_file":"%s" + }`, + c.KeyMetadataFile, + c.KeyPolicyFile, + c.KeyRing, + c.ServiceAccountFile), + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "test.example.org"}, + } +} + +func configureRequestWithDefaults(t *testing.T) *configv1.ConfigureRequest { + return &configv1.ConfigureRequest{ + HclConfiguration: serializedConfiguration(createKeyMetadataFile(t, validServerID), validKeyRing), + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "test.example.org"}, + } +} + +func configureRequestWithString(config string) *configv1.ConfigureRequest { + return &configv1.ConfigureRequest{ + HclConfiguration: config, + } +} + +func configureRequestWithVars(keyMetadataFile, keyPolicyFile, keyRing, serviceAccountFile string) *configv1.ConfigureRequest { + return &configv1.ConfigureRequest{ + HclConfiguration: fmt.Sprintf(`{ + "key_metadata_file":"%s", + "key_policy_file":"%s", + "key_ring":"%s" + "service_account_file":"%s" + }`, + keyMetadataFile, + keyPolicyFile, + keyRing, + serviceAccountFile), + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "test.example.org"}, + } +} + +func createKeyMetadataFile(t *testing.T, content string) string { + tempDir := t.TempDir() + tempFilePath := filepath.ToSlash(filepath.Join(tempDir, validServerIDFile)) + + if content != "" { + err := os.WriteFile(tempFilePath, []byte(content), 0600) + if err != nil { + t.Error(err) + } + } + return tempFilePath +} + +func getCustomPolicyFile(t *testing.T) string { + tempDir := t.TempDir() + tempFilePath := filepath.ToSlash(filepath.Join(tempDir, validPolicyFile)) + err := os.WriteFile(tempFilePath, []byte(customPolicy), 0600) + if err != nil { + t.Error(err) + } + return tempFilePath +} + +func serializedConfiguration(keyMetadataFile, keyRing string) string { + return fmt.Sprintf(`{ + "key_metadata_file":"%s", + "key_ring":"%s" + }`, + keyMetadataFile, + keyRing) +} + +func waitForSignal(t *testing.T, ch chan error) error { + select { + case err := <-ch: + return err + case <-time.After(testTimeout): + t.Fail() + } + return nil +} diff --git a/pkg/server/plugin/keymanager/memory/memory.go b/pkg/server/plugin/keymanager/memory/memory.go index a06ae34f4f..76c12f96cf 100644 --- a/pkg/server/plugin/keymanager/memory/memory.go +++ b/pkg/server/plugin/keymanager/memory/memory.go @@ -6,11 +6,17 @@ import ( keymanagerbase "github.com/spiffe/spire/pkg/server/plugin/keymanager/base" ) +type Generator = keymanagerbase.Generator + func BuiltIn() catalog.BuiltIn { - return builtin(New()) + return asBuiltIn(newKeyManager(nil)) +} + +func TestBuiltIn(generator Generator) catalog.BuiltIn { + return asBuiltIn(newKeyManager(generator)) } -func builtin(p *KeyManager) catalog.BuiltIn { +func asBuiltIn(p *KeyManager) catalog.BuiltIn { return catalog.MakeBuiltIn("memory", keymanagerv1.KeyManagerPluginServer(p)) } @@ -18,8 +24,10 @@ type KeyManager struct { *keymanagerbase.Base } -func New() *KeyManager { +func newKeyManager(generator Generator) *KeyManager { return &KeyManager{ - Base: keymanagerbase.New(keymanagerbase.Funcs{}), + Base: keymanagerbase.New(keymanagerbase.Config{ + Generator: generator, + }), } } diff --git a/pkg/server/plugin/keymanager/memory/memory_test.go b/pkg/server/plugin/keymanager/memory/memory_test.go index 205fd2f3c1..11491d890a 100644 --- a/pkg/server/plugin/keymanager/memory/memory_test.go +++ b/pkg/server/plugin/keymanager/memory/memory_test.go @@ -13,7 +13,7 @@ func TestKeyManagerContract(t *testing.T) { keymanagertest.Test(t, keymanagertest.Config{ Create: func(t *testing.T) keymanager.KeyManager { km := new(keymanager.V1) - plugintest.Load(t, memory.BuiltIn(), km) + plugintest.Load(t, memory.TestBuiltIn(keymanagertest.NewGenerator()), km) return km }, }) diff --git a/pkg/server/plugin/keymanager/test/keymanagertest.go b/pkg/server/plugin/keymanager/test/keymanagertest.go index cc6773ed21..058c7e2df7 100644 --- a/pkg/server/plugin/keymanager/test/keymanagertest.go +++ b/pkg/server/plugin/keymanager/test/keymanagertest.go @@ -10,11 +10,15 @@ import ( "crypto/sha256" "crypto/x509" "math/big" + "os" + "strconv" "testing" "github.com/spiffe/spire/pkg/common/plugin" "github.com/spiffe/spire/pkg/server/plugin/keymanager" + keymanagerbase "github.com/spiffe/spire/pkg/server/plugin/keymanager/base" "github.com/spiffe/spire/test/spiretest" + "github.com/spiffe/spire/test/testkey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -48,6 +52,13 @@ var ( } ) +func NewGenerator() keymanagerbase.Generator { + if nightly, err := strconv.ParseBool(os.Getenv("NIGHTLY")); err == nil && nightly { + return nil + } + return &testkey.Generator{} +} + type CreateFunc = func(t *testing.T) keymanager.KeyManager type Config struct { diff --git a/pkg/server/plugin/nodeattestor/awsiid/iid.go b/pkg/server/plugin/nodeattestor/awsiid/iid.go index 73e969254a..a1e3402310 100644 --- a/pkg/server/plugin/nodeattestor/awsiid/iid.go +++ b/pkg/server/plugin/nodeattestor/awsiid/iid.go @@ -54,7 +54,15 @@ const ( // accessKeyIDVarName env var name for AWS access key ID accessKeyIDVarName = "AWS_ACCESS_KEY_ID" // secretAccessKeyVarName env car name for AWS secret access key - secretAccessKeyVarName = "AWS_SECRET_ACCESS_KEY" //nolint: gosec // false positive + secretAccessKeyVarName = "AWS_SECRET_ACCESS_KEY" //nolint: gosec // false positive + azSelectorPrefix = "az" + imageIDSelectorPrefix = "image:id" + instanceIDSelectorPrefix = "instance:id" + regionSelectorPrefix = "region" + sgIDSelectorPrefix = "sg:id" + sgNameSelectorPrefix = "sg:name" + tagSelectorPrefix = "tag" + iamRoleSelectorPrefix = "iamrole" ) // BuiltIn creates a new built-in plugin @@ -192,7 +200,7 @@ func (p *IIDAttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServ return err } - selectorValues, err := p.resolveSelectors(stream.Context(), instancesDesc, awsClient) + selectorValues, err := p.resolveSelectors(stream.Context(), instancesDesc, attestationData, awsClient) if err != nil { return err } @@ -352,7 +360,7 @@ func unmarshalAndValidateIdentityDocument(data []byte, pubKey *rsa.PublicKey) (i return doc, nil } -func (p *IIDAttestorPlugin) resolveSelectors(parent context.Context, instancesDesc *ec2.DescribeInstancesOutput, client Client) ([]string, error) { +func (p *IIDAttestorPlugin) resolveSelectors(parent context.Context, instancesDesc *ec2.DescribeInstancesOutput, iiDoc imds.InstanceIdentityDocument, client Client) ([]string, error) { selectorSet := map[string]bool{} addSelectors := func(values []string) { for _, value := range values { @@ -386,6 +394,8 @@ func (p *IIDAttestorPlugin) resolveSelectors(parent context.Context, instancesDe } } + resolveIIDocSelectors(selectorSet, iiDoc) + // build and sort selectors selectors := []string{} for value := range selectorSet { @@ -396,10 +406,17 @@ func (p *IIDAttestorPlugin) resolveSelectors(parent context.Context, instancesDe return selectors, nil } +func resolveIIDocSelectors(selectorSet map[string]bool, iiDoc imds.InstanceIdentityDocument) { + selectorSet[fmt.Sprintf("%s:%s", imageIDSelectorPrefix, iiDoc.ImageID)] = true + selectorSet[fmt.Sprintf("%s:%s", instanceIDSelectorPrefix, iiDoc.InstanceID)] = true + selectorSet[fmt.Sprintf("%s:%s", regionSelectorPrefix, iiDoc.Region)] = true + selectorSet[fmt.Sprintf("%s:%s", azSelectorPrefix, iiDoc.AvailabilityZone)] = true +} + func resolveTags(tags []ec2types.Tag) []string { values := make([]string, 0, len(tags)) for _, tag := range tags { - values = append(values, fmt.Sprintf("tag:%s:%s", aws.ToString(tag.Key), aws.ToString(tag.Value))) + values = append(values, fmt.Sprintf("%s:%s:%s", tagSelectorPrefix, aws.ToString(tag.Key), aws.ToString(tag.Value))) } return values } @@ -408,8 +425,8 @@ func resolveSecurityGroups(sgs []ec2types.GroupIdentifier) []string { values := make([]string, 0, len(sgs)*2) for _, sg := range sgs { values = append(values, - fmt.Sprintf("sg:id:%s", aws.ToString(sg.GroupId)), - fmt.Sprintf("sg:name:%s", aws.ToString(sg.GroupName)), + fmt.Sprintf("%s:%s", sgIDSelectorPrefix, aws.ToString(sg.GroupId)), + fmt.Sprintf("%s:%s", sgNameSelectorPrefix, aws.ToString(sg.GroupName)), ) } return values @@ -422,7 +439,7 @@ func resolveInstanceProfile(instanceProfile *iamtypes.InstanceProfile) []string values := make([]string, 0, len(instanceProfile.Roles)) for _, role := range instanceProfile.Roles { if role.Arn != nil { - values = append(values, fmt.Sprintf("iamrole:%s", aws.ToString(role.Arn))) + values = append(values, fmt.Sprintf("%s:%s", iamRoleSelectorPrefix, aws.ToString(role.Arn))) } } return values diff --git a/pkg/server/plugin/nodeattestor/awsiid/iid_test.go b/pkg/server/plugin/nodeattestor/awsiid/iid_test.go index bc33469310..8c3fc564be 100644 --- a/pkg/server/plugin/nodeattestor/awsiid/iid_test.go +++ b/pkg/server/plugin/nodeattestor/awsiid/iid_test.go @@ -43,15 +43,17 @@ const ( ) var ( - testAWSCAKey = testkey.MustRSA2048() - testInstance = "test-instance" - testAccount = "test-account" - testRegion = "test-region" - testProfile = "test-profile" - zeroDeviceIndex = int32(0) - nonzeroDeviceIndex = int32(1) - instanceStoreType = ec2types.DeviceTypeInstanceStore - ebsType = ec2types.DeviceTypeEbs + testAWSCAKey = testkey.MustRSA2048() + testInstance = "test-instance" + testAccount = "test-account" + testRegion = "test-region" + testAvailabilityZone = "test-az" + testImageID = "test-image-id" + testProfile = "test-profile" + zeroDeviceIndex = int32(0) + nonzeroDeviceIndex = int32(1) + instanceStoreType = ec2types.DeviceTypeInstanceStore + ebsType = ec2types.DeviceTypeEbs ) func TestAttest(t *testing.T) { @@ -141,6 +143,12 @@ func TestAttest(t *testing.T) { { name: "success with zero device index", expectID: "spiffe://example.org/spire/agent/aws_iid/test-account/test-region/test-instance", + expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, + }, }, { name: "success with non-zero device index when check is disabled", @@ -149,6 +157,12 @@ func TestAttest(t *testing.T) { output.Reservations[0].Instances[0].NetworkInterfaces[0].Attachment.DeviceIndex = &nonzeroDeviceIndex }, expectID: "spiffe://example.org/spire/agent/aws_iid/test-account/test-region/test-instance", + expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, + }, }, { name: "success with non-zero device index when local account is allow-listed", @@ -157,6 +171,12 @@ func TestAttest(t *testing.T) { output.Reservations[0].Instances[0].NetworkInterfaces[0].Attachment.DeviceIndex = &nonzeroDeviceIndex }, expectID: "spiffe://example.org/spire/agent/aws_iid/test-account/test-region/test-instance", + expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, + }, }, { name: "block device anti-tampering check rejects non-zero network device index", @@ -215,11 +235,23 @@ func TestAttest(t *testing.T) { output.Reservations[0].Instances[0].NetworkInterfaces[0].Attachment.AttachTime = aws.Time(interfaceAttachTime) }, expectID: "spiffe://example.org/spire/agent/aws_iid/test-account/test-region/test-instance", + expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, + }, }, { name: "success with agent_path_template", config: `agent_path_template = "/{{ .PluginName }}/custom/{{ .AccountID }}/{{ .Region }}/{{ .InstanceID }}"`, expectID: "spiffe://example.org/spire/agent/aws_iid/custom/test-account/test-region/test-instance", + expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, + }, }, { name: "success with tags in template", @@ -231,9 +263,15 @@ func TestAttest(t *testing.T) { }, } }, - config: `agent_path_template = "/{{ .PluginName }}/zone1/{{ .Tags.Hostname }}"`, - expectID: "spiffe://example.org/spire/agent/aws_iid/zone1/host1", - expectSelectors: []*common.Selector{{Type: "aws_iid", Value: "tag:Hostname:host1"}}, + config: `agent_path_template = "/{{ .PluginName }}/zone1/{{ .Tags.Hostname }}"`, + expectID: "spiffe://example.org/spire/agent/aws_iid/zone1/host1", + expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, + {Type: caws.PluginName, Value: "tag:Hostname:host1"}, + }, }, { name: "fails with missing tags in template", @@ -270,8 +308,12 @@ func TestAttest(t *testing.T) { }, expectID: "spiffe://example.org/spire/agent/aws_iid/test-account/test-region/test-instance", expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, {Type: caws.PluginName, Value: "iamrole:role1"}, {Type: caws.PluginName, Value: "iamrole:role2"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, {Type: caws.PluginName, Value: "sg:id:TestGroup"}, {Type: caws.PluginName, Value: "sg:name:Test Group Name"}, {Type: caws.PluginName, Value: "tag:Hostname:host1"}, @@ -307,6 +349,10 @@ func TestAttest(t *testing.T) { }, expectID: "spiffe://example.org/spire/agent/aws_iid/test-account/test-region/test-instance", expectSelectors: []*common.Selector{ + {Type: caws.PluginName, Value: "az:test-az"}, + {Type: caws.PluginName, Value: "image:id:test-image-id"}, + {Type: caws.PluginName, Value: "instance:id:test-instance"}, + {Type: caws.PluginName, Value: "region:test-region"}, {Type: caws.PluginName, Value: "sg:id:TestGroup"}, {Type: caws.PluginName, Value: "sg:name:Test Group Name"}, {Type: caws.PluginName, Value: "tag:Hostname:host1"}, @@ -525,9 +571,11 @@ func (c *fakeClient) GetInstanceProfile(ctx context.Context, input *iam.GetInsta func buildAttestationData(t *testing.T) caws.IIDAttestationData { // doc body doc := imds.InstanceIdentityDocument{ - AccountID: testAccount, - InstanceID: testInstance, - Region: testRegion, + AccountID: testAccount, + InstanceID: testInstance, + Region: testRegion, + AvailabilityZone: testAvailabilityZone, + ImageID: testImageID, } docBytes, err := json.Marshal(doc) require.NoError(t, err) diff --git a/pkg/server/plugin/nodeattestor/azuremsi/msi.go b/pkg/server/plugin/nodeattestor/azuremsi/msi.go index 0ff13cc3ad..a4cfbc1da1 100644 --- a/pkg/server/plugin/nodeattestor/azuremsi/msi.go +++ b/pkg/server/plugin/nodeattestor/azuremsi/msi.go @@ -20,6 +20,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" nodeattestorv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/nodeattestor/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" + "github.com/spiffe/spire/pkg/common/agentpathtemplate" "github.com/spiffe/spire/pkg/common/catalog" "github.com/spiffe/spire/pkg/common/jwtutil" "github.com/spiffe/spire/pkg/common/plugin/azure" @@ -69,7 +70,8 @@ type TenantConfig struct { } type MSIAttestorConfig struct { - Tenants map[string]*TenantConfig `hcl:"tenants" json:"tenants"` + Tenants map[string]*TenantConfig `hcl:"tenants" json:"tenants"` + AgentPathTemplate string `hcl:"agent_path_template" json:"agent_path_template"` } type tenantConfig struct { @@ -78,8 +80,9 @@ type tenantConfig struct { } type msiAttestorConfig struct { - td spiffeid.TrustDomain - tenants map[string]*tenantConfig + td spiffeid.TrustDomain + tenants map[string]*tenantConfig + idPathTemplate *agentpathtemplate.Template } type MSIAttestorPlugin struct { @@ -168,17 +171,22 @@ func (p *MSIAttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServ if err := token.Claims(&keys[0], claims); err != nil { return status.Errorf(codes.InvalidArgument, "unable to verify token: %v", err) } + switch { case claims.TenantID == "": return status.Error(codes.Internal, "token missing tenant ID claim") - case claims.Subject == "": + case claims.PrincipalID == "": return status.Error(codes.Internal, "token missing subject claim") } // Before doing the work to validate the token, ensure that this MSI token // has not already been used to attest an agent. - agentID := claims.AgentID(config.td.String()) - if err := p.AssessTOFU(stream.Context(), agentID, p.log); err != nil { + agentID, err := azure.MakeAgentID(config.td, config.idPathTemplate, claims) + if err != nil { + return status.Errorf(codes.Internal, "unable to make agent ID: %v", err) + } + + if err := p.AssessTOFU(stream.Context(), agentID.String(), p.log); err != nil { return err } @@ -196,7 +204,7 @@ func (p *MSIAttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServ var selectorValues []string if tenant.client != nil { - selectorValues, err = p.resolve(stream.Context(), tenant.client, claims.Subject) + selectorValues, err = p.resolve(stream.Context(), tenant.client, claims.PrincipalID) if err != nil { return err } @@ -205,7 +213,7 @@ func (p *MSIAttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServ return stream.Send(&nodeattestorv1.AttestResponse{ Response: &nodeattestorv1.AttestResponse_AgentAttributes{ AgentAttributes: &nodeattestorv1.AgentAttributes{ - SpiffeId: agentID, + SpiffeId: agentID.String(), CanReattest: false, SelectorValues: selectorValues, }, @@ -297,9 +305,19 @@ func (p *MSIAttestorPlugin) Configure(ctx context.Context, req *configv1.Configu } } + tmpl := azure.DefaultAgentPathTemplate + if len(hclConfig.AgentPathTemplate) > 0 { + var err error + tmpl, err = agentpathtemplate.Parse(hclConfig.AgentPathTemplate) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse agent path template: %q", hclConfig.AgentPathTemplate) + } + } + p.setConfig(&msiAttestorConfig{ - td: td, - tenants: tenants, + td: td, + tenants: tenants, + idPathTemplate: tmpl, }) return &configv1.ConfigureResponse{}, nil } diff --git a/pkg/server/plugin/nodeattestor/azuremsi/msi_test.go b/pkg/server/plugin/nodeattestor/azuremsi/msi_test.go index 39cc16d084..2a1b6a00d7 100644 --- a/pkg/server/plugin/nodeattestor/azuremsi/msi_test.go +++ b/pkg/server/plugin/nodeattestor/azuremsi/msi_test.go @@ -235,6 +235,44 @@ func (s *MSIAttestorSuite) TestAttestSuccessWithCustomResourceID() { vmSelectors) } +func (s *MSIAttestorSuite) TestAttestSuccessWithCustomSPIFFEIDTemplate() { + s.setVirtualMachine(&armcompute.VirtualMachine{ + Properties: &armcompute.VirtualMachineProperties{}, + }) + + payload := s.signAttestPayload("KEYID", resourceID, "TENANTID", "PRINCIPALID") + + selectorValues := append([]string{}, vmSelectors...) + sort.Strings(selectorValues) + + var expected []*common.Selector + for _, selectorValue := range selectorValues { + expected = append(expected, &common.Selector{ + Type: "azure_msi", + Value: selectorValue, + }) + } + + attestorWithCustomAgentTemplate := s.loadPluginWithConfig( + ` + tenants = { + "TENANTID" = { + resource_id = "https://example.org/app/" + use_msi = true + } + "TENANTID2" = { + use_msi = true + } + } + agent_path_template = "/{{ .PluginName }}/{{ .TenantID }}" + `) + resp, err := attestorWithCustomAgentTemplate.Attest(context.Background(), payload, expectNoChallenge) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal("spiffe://example.org/spire/agent/azure_msi/TENANTID", resp.AgentID) + s.RequireProtoListEqual(expected, resp.Selectors) +} + func (s *MSIAttestorSuite) TestAttestSuccessWithNoClientCredentials() { s.attestor = s.loadPlugin(plugintest.Configure(` tenants = { @@ -586,6 +624,20 @@ func (s *MSIAttestorSuite) signAttestPayload(keyID, audience, tenantID, principa } func (s *MSIAttestorSuite) loadPlugin(options ...plugintest.Option) nodeattestor.NodeAttestor { + return s.loadPluginWithConfig(` + tenants = { + "TENANTID" = { + resource_id = "https://example.org/app/" + use_msi = true + } + "TENANTID2" = { + use_msi = true + } + } + `, options...) +} + +func (s *MSIAttestorSuite) loadPluginWithConfig(config string, options ...plugintest.Option) nodeattestor.NodeAttestor { attestor := New() attestor.hooks.now = func() time.Time { return s.now @@ -609,17 +661,7 @@ func (s *MSIAttestorSuite) loadPlugin(options ...plugintest.Option) nodeattestor plugintest.CoreConfig(catalog.CoreConfig{ TrustDomain: spiffeid.RequireTrustDomainFromString("example.org"), }), - plugintest.Configure(` - tenants = { - "TENANTID" = { - resource_id = "https://example.org/app/" - use_msi = true - } - "TENANTID2" = { - use_msi = true - } - } - `), + plugintest.Configure(config), }, options...)...) return v1 } diff --git a/pkg/server/plugin/nodeattestor/tpmdevid/devid_test.go b/pkg/server/plugin/nodeattestor/tpmdevid/devid_test.go index 8224a1f0ef..47ef6deeaf 100644 --- a/pkg/server/plugin/nodeattestor/tpmdevid/devid_test.go +++ b/pkg/server/plugin/nodeattestor/tpmdevid/devid_test.go @@ -1,3 +1,6 @@ +//go:build !darwin +// +build !darwin + package tpmdevid_test import ( diff --git a/pkg/server/plugin/notifier/k8sbundle/k8sbundle.go b/pkg/server/plugin/notifier/k8sbundle/k8sbundle.go index 37a5177311..2894ccc0e6 100644 --- a/pkg/server/plugin/notifier/k8sbundle/k8sbundle.go +++ b/pkg/server/plugin/notifier/k8sbundle/k8sbundle.go @@ -160,7 +160,10 @@ func (p *Plugin) startInformers(ctx context.Context, config *pluginConfig, clien if config.WebhookLabel != "" || config.APIServiceLabel != "" { informerSynced := []cache.InformerSynced{} for _, client := range clients { - informer := client.Informer(p.hooks.informerCallback) + informer, err := client.Informer(p.hooks.informerCallback) + if err != nil { + return err + } if informer != nil { go informer.Run(stopCh) informerSynced = append(informerSynced, informer.HasSynced) @@ -421,7 +424,7 @@ type kubeClient interface { GetList(ctx context.Context) (runtime.Object, error) CreatePatch(ctx context.Context, obj runtime.Object, resp *identityproviderv1.FetchX509IdentityResponse) (runtime.Object, error) Patch(ctx context.Context, namespace, name string, patchBytes []byte) error - Informer(callback informerCallback) cache.SharedIndexInformer + Informer(callback informerCallback) (cache.SharedIndexInformer, error) } // configMapClient encapsulates the Kubernetes API for updating the CA Bundle in a config map @@ -467,11 +470,11 @@ func (c configMapClient) Patch(ctx context.Context, namespace, name string, patc return err } -func (c configMapClient) Informer(callback informerCallback) cache.SharedIndexInformer { - return nil +func (c configMapClient) Informer(callback informerCallback) (cache.SharedIndexInformer, error) { + return nil, nil } -// apiServiceClient encapsulates the Kubenetes API for updating the CA Bundle in an API Service +// apiServiceClient encapsulates the Kubernetes API for updating the CA Bundle in an API Service type apiServiceClient struct { aggregator.Interface apiServiceLabel string @@ -518,9 +521,11 @@ func (c apiServiceClient) Patch(ctx context.Context, namespace, name string, pat return err } -func (c apiServiceClient) Informer(callback informerCallback) cache.SharedIndexInformer { +func (c apiServiceClient) Informer(callback informerCallback) (cache.SharedIndexInformer, error) { informer := c.factory.Apiregistration().V1().APIServices().Informer() - informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + // AddEventHandler now support returning event handler registration, + // to remove them if required (https://github.com/kubernetes-sigs/controller-runtime/pull/2046) + _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { callback(c, obj.(runtime.Object)) }, @@ -528,10 +533,13 @@ func (c apiServiceClient) Informer(callback informerCallback) cache.SharedIndexI callback(c, newObj.(runtime.Object)) }, }) - return informer + if err != nil { + return nil, err + } + return informer, nil } -// mutatingWebhookClient encapsulates the Kubenetes API for updating the CA Bundle in a mutating webhook +// mutatingWebhookClient encapsulates the Kubernetes API for updating the CA Bundle in a mutating webhook type mutatingWebhookClient struct { kubernetes.Interface webhookLabel string @@ -589,9 +597,9 @@ func (c mutatingWebhookClient) Patch(ctx context.Context, namespace, name string return err } -func (c mutatingWebhookClient) Informer(callback informerCallback) cache.SharedIndexInformer { +func (c mutatingWebhookClient) Informer(callback informerCallback) (cache.SharedIndexInformer, error) { informer := c.factory.Admissionregistration().V1().MutatingWebhookConfigurations().Informer() - informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { callback(c, obj.(runtime.Object)) }, @@ -599,10 +607,13 @@ func (c mutatingWebhookClient) Informer(callback informerCallback) cache.SharedI callback(c, newObj.(runtime.Object)) }, }) - return informer + if err != nil { + return nil, err + } + return informer, nil } -// validatingWebhookClient encapsulates the Kubenetes API for updating the CA Bundle in a validating webhook +// validatingWebhookClient encapsulates the Kubernetes API for updating the CA Bundle in a validating webhook type validatingWebhookClient struct { kubernetes.Interface webhookLabel string @@ -660,9 +671,9 @@ func (c validatingWebhookClient) Patch(ctx context.Context, namespace, name stri return err } -func (c validatingWebhookClient) Informer(callback informerCallback) cache.SharedIndexInformer { +func (c validatingWebhookClient) Informer(callback informerCallback) (cache.SharedIndexInformer, error) { informer := c.factory.Admissionregistration().V1().ValidatingWebhookConfigurations().Informer() - informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { callback(c, obj.(runtime.Object)) }, @@ -670,7 +681,10 @@ func (c validatingWebhookClient) Informer(callback informerCallback) cache.Share callback(c, newObj.(runtime.Object)) }, }) - return informer + if err != nil { + return nil, err + } + return informer, nil } // bundleData formats the bundle data for inclusion in the config map diff --git a/pkg/server/plugin/notifier/k8sbundle/k8sbundle_test.go b/pkg/server/plugin/notifier/k8sbundle/k8sbundle_test.go index c80f2edcf0..af5df0e414 100644 --- a/pkg/server/plugin/notifier/k8sbundle/k8sbundle_test.go +++ b/pkg/server/plugin/notifier/k8sbundle/k8sbundle_test.go @@ -773,8 +773,8 @@ func (c *fakeKubeClient) Patch(ctx context.Context, namespace, configMap string, return nil } -func (c *fakeKubeClient) Informer(callback informerCallback) cache.SharedIndexInformer { - return nil +func (c *fakeKubeClient) Informer(callback informerCallback) (cache.SharedIndexInformer, error) { + return nil, nil } func (c *fakeKubeClient) getConfigMap(namespace, configMap string) *corev1.ConfigMap { diff --git a/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas.go b/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas.go index a15171fd8a..fb66f8b0b4 100644 --- a/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas.go +++ b/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas.go @@ -11,7 +11,8 @@ import ( "sync" "time" - pcaapi "cloud.google.com/go/security/privateca/apiv1" + privateca "cloud.google.com/go/security/privateca/apiv1" + "cloud.google.com/go/security/privateca/apiv1/privatecapb" "github.com/hashicorp/go-hclog" "github.com/hashicorp/hcl" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -22,7 +23,6 @@ import ( "github.com/spiffe/spire/pkg/common/pemutil" "github.com/spiffe/spire/pkg/common/x509util" "google.golang.org/api/iterator" - privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" @@ -373,7 +373,7 @@ func (p *Plugin) mintX509CA(ctx context.Context, csr []byte, preferredTTL int32) func getClient(ctx context.Context) (CAClient, error) { // https://cloud.google.com/docs/authentication/production#go // The client creation implicitly uses Application Default Credentials (ADC) for authentication - pcaClient, err := pcaapi.NewCertificateAuthorityClient(ctx) + pcaClient, err := privateca.NewCertificateAuthorityClient(ctx) if err != nil { return nil, err } @@ -382,7 +382,7 @@ func getClient(ctx context.Context) (CAClient, error) { } type gcpCAClient struct { - pcaClient *pcaapi.CertificateAuthorityClient + pcaClient *privateca.CertificateAuthorityClient } func (client *gcpCAClient) CreateCertificate(ctx context.Context, req *privatecapb.CreateCertificateRequest) (*privatecapb.Certificate, error) { diff --git a/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas_test.go b/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas_test.go index 92979d8659..ad05ca295a 100644 --- a/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas_test.go +++ b/pkg/server/plugin/upstreamauthority/gcpcas/gcpcas_test.go @@ -11,13 +11,13 @@ import ( "testing" "time" + "cloud.google.com/go/security/privateca/apiv1/privatecapb" "github.com/spiffe/spire/pkg/common/pemutil" commonutil "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/server/plugin/upstreamauthority" "github.com/spiffe/spire/test/plugintest" "github.com/spiffe/spire/test/testkey" "github.com/stretchr/testify/require" - privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go b/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go index 8fe65e9656..1217cd1c8c 100644 --- a/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go +++ b/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go @@ -23,7 +23,7 @@ import ( "google.golang.org/grpc/status" ) -// newServerClient creates a new spire-sever client +// newServerClient creates a new spire-server client func newServerClient(serverID spiffeid.ID, serverAddr string, workloadAPIAddr net.Addr, log hclog.Logger) *serverClient { return &serverClient{ serverID: serverID, diff --git a/pkg/server/plugin/upstreamauthority/spire/spire_test.go b/pkg/server/plugin/upstreamauthority/spire/spire_test.go index 9a92fdce12..99ad2425f3 100644 --- a/pkg/server/plugin/upstreamauthority/spire/spire_test.go +++ b/pkg/server/plugin/upstreamauthority/spire/spire_test.go @@ -130,7 +130,7 @@ func TestMintX509CA(t *testing.T) { svidCert, svidKey, err := s.MarshalRaw() require.NoError(t, err) - // Create sever's CA + // Create server's CA serverCert, serverKey := ca.CreateX509Certificate( testca.WithID(spiffeid.RequireFromPath(trustDomain, "/spire/server")), ) diff --git a/pkg/server/registration/manager.go b/pkg/server/registration/manager.go index 40fdbeafab..8d5ba5ec29 100644 --- a/pkg/server/registration/manager.go +++ b/pkg/server/registration/manager.go @@ -12,7 +12,7 @@ import ( ) const ( - _pruningCandence = 5 * time.Minute + _pruningCadence = 5 * time.Minute ) // ManagerConfig is the config for the registration manager @@ -40,7 +40,7 @@ func NewManager(c ManagerConfig) *Manager { return &Manager{ c: c, - log: c.Log.WithField(telemetry.RetryInterval, _pruningCandence), + log: c.Log.WithField(telemetry.RetryInterval, _pruningCadence), metrics: c.Metrics, } } @@ -51,7 +51,7 @@ func (m *Manager) Run(ctx context.Context) error { } func (m *Manager) pruneEvery(ctx context.Context) error { - ticker := m.c.Clock.Ticker(_pruningCandence) + ticker := m.c.Clock.Ticker(_pruningCadence) defer ticker.Stop() for { diff --git a/pkg/server/registration/manager_test.go b/pkg/server/registration/manager_test.go index c9ab36ba0d..d71dcf66dd 100644 --- a/pkg/server/registration/manager_test.go +++ b/pkg/server/registration/manager_test.go @@ -42,7 +42,7 @@ func (s *ManagerSuite) TestPruning() { done := s.setupAndRunManager() defer done() - expiry := s.clock.Now().Add(_pruningCandence) + expiry := s.clock.Now().Add(_pruningCadence) // expires right on the pruning time entry1 := &common.RegistrationEntry{ @@ -105,7 +105,7 @@ func (s *ManagerSuite) TestPruning() { s.Equal([]*common.RegistrationEntry{registrationEntry1, registrationEntry2, registrationEntry3}, listResp.Entries) // prune first entry - s.clock.Add(_pruningCandence + time.Second) + s.clock.Add(_pruningCadence + time.Second) s.NoError(s.m.prune(context.Background())) listResp, err = s.ds.ListRegistrationEntries(context.Background(), &datastore.ListRegistrationEntriesRequest{}) s.NoError(err) diff --git a/pkg/server/server.go b/pkg/server/server.go index 79a0e303af..214f550d2a 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -265,7 +265,8 @@ func (s *Server) loadCatalog(ctx context.Context, metrics telemetry.Metrics, ide func (s *Server) newCA(metrics telemetry.Metrics, healthChecker health.Checker) *ca.CA { return ca.NewCA(ca.Config{ Metrics: metrics, - X509SVIDTTL: s.config.SVIDTTL, + X509SVIDTTL: s.config.X509SVIDTTL, + JWTSVIDTTL: s.config.JWTSVIDTTL, JWTIssuer: s.config.JWTIssuer, TrustDomain: s.config.TrustDomain, CASubject: s.config.CASubject, @@ -352,7 +353,7 @@ func (s *Server) newBundleManager(cat catalog.Catalog, metrics telemetry.Metrics Metrics: metrics, DataStore: cat.GetDataStore(), Source: bundle_client.MergeTrustDomainConfigSources( - bundle_client.TrustDomainConfigMap(s.config.Federation.FederatesWith), + bundle_client.NewTrustDomainConfigSet(s.config.Federation.FederatesWith), bundle_client.DataStoreTrustDomainConfigSource(log, cat.GetDataStore()), ), }) diff --git a/proto/spire/common/common.pb.go b/proto/spire/common/common.pb.go index c53381f492..f928afa8fd 100644 --- a/proto/spire/common/common.pb.go +++ b/proto/spire/common/common.pb.go @@ -354,8 +354,8 @@ type RegistrationEntry struct { // caller. It is defined as a URI comprising a “trust domain” and an // associated path. SpiffeId string `protobuf:"bytes,3,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` - // * Time to live. - Ttl int32 `protobuf:"varint,4,opt,name=ttl,proto3" json:"ttl,omitempty"` + // * Time to live for X509-SVIDs generated from this entry. Was previously called 'ttl'. + X509SvidTtl int32 `protobuf:"varint,4,opt,name=x509_svid_ttl,json=x509SvidTtl,proto3" json:"x509_svid_ttl,omitempty"` // * A list of federated trust domain SPIFFE IDs. FederatesWith []string `protobuf:"bytes,5,rep,name=federates_with,json=federatesWith,proto3" json:"federates_with,omitempty"` // * Entry ID @@ -374,6 +374,8 @@ type RegistrationEntry struct { RevisionNumber int64 `protobuf:"varint,11,opt,name=revision_number,json=revisionNumber,proto3" json:"revision_number,omitempty"` // * Determines if the issued SVID must be stored through an SVIDStore plugin StoreSvid bool `protobuf:"varint,12,opt,name=store_svid,json=storeSvid,proto3" json:"store_svid,omitempty"` + // * Time to live for JWT-SVIDs generated from this entry, if set will override ttl field. + JwtSvidTtl int32 `protobuf:"varint,13,opt,name=jwt_svid_ttl,json=jwtSvidTtl,proto3" json:"jwt_svid_ttl,omitempty"` } func (x *RegistrationEntry) Reset() { @@ -429,9 +431,9 @@ func (x *RegistrationEntry) GetSpiffeId() string { return "" } -func (x *RegistrationEntry) GetTtl() int32 { +func (x *RegistrationEntry) GetX509SvidTtl() int32 { if x != nil { - return x.Ttl + return x.X509SvidTtl } return 0 } @@ -492,6 +494,13 @@ func (x *RegistrationEntry) GetStoreSvid() bool { return false } +func (x *RegistrationEntry) GetJwtSvidTtl() int32 { + if x != nil { + return x.JwtSvidTtl + } + return 0 +} + // * The RegistrationEntryMask is used to update only selected fields of the RegistrationEntry type RegistrationEntryMask struct { state protoimpl.MessageState @@ -501,7 +510,7 @@ type RegistrationEntryMask struct { Selectors bool `protobuf:"varint,1,opt,name=selectors,proto3" json:"selectors,omitempty"` ParentId bool `protobuf:"varint,2,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty"` SpiffeId bool `protobuf:"varint,3,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` - Ttl bool `protobuf:"varint,4,opt,name=ttl,proto3" json:"ttl,omitempty"` + X509SvidTtl bool `protobuf:"varint,4,opt,name=x509_svid_ttl,json=x509SvidTtl,proto3" json:"x509_svid_ttl,omitempty"` FederatesWith bool `protobuf:"varint,5,opt,name=federates_with,json=federatesWith,proto3" json:"federates_with,omitempty"` EntryId bool `protobuf:"varint,6,opt,name=entry_id,json=entryId,proto3" json:"entry_id,omitempty"` Admin bool `protobuf:"varint,7,opt,name=admin,proto3" json:"admin,omitempty"` @@ -509,6 +518,7 @@ type RegistrationEntryMask struct { EntryExpiry bool `protobuf:"varint,9,opt,name=entryExpiry,proto3" json:"entryExpiry,omitempty"` DnsNames bool `protobuf:"varint,10,opt,name=dns_names,json=dnsNames,proto3" json:"dns_names,omitempty"` StoreSvid bool `protobuf:"varint,11,opt,name=store_svid,json=storeSvid,proto3" json:"store_svid,omitempty"` + JwtSvidTtl bool `protobuf:"varint,12,opt,name=jwt_svid_ttl,json=jwtSvidTtl,proto3" json:"jwt_svid_ttl,omitempty"` } func (x *RegistrationEntryMask) Reset() { @@ -564,9 +574,9 @@ func (x *RegistrationEntryMask) GetSpiffeId() bool { return false } -func (x *RegistrationEntryMask) GetTtl() bool { +func (x *RegistrationEntryMask) GetX509SvidTtl() bool { if x != nil { - return x.Ttl + return x.X509SvidTtl } return false } @@ -620,6 +630,13 @@ func (x *RegistrationEntryMask) GetStoreSvid() bool { return false } +func (x *RegistrationEntryMask) GetJwtSvidTtl() bool { + if x != nil { + return x.JwtSvidTtl + } + return false +} + // * A list of registration entries. type RegistrationEntries struct { state protoimpl.MessageState @@ -1050,7 +1067,7 @@ var file_spire_common_common_proto_rawDesc = []byte{ 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x09, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x61, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x61, 0x74, 0x74, - 0x65, 0x73, 0x74, 0x22, 0x94, 0x03, 0x0a, 0x11, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x65, 0x73, 0x74, 0x22, 0xc8, 0x03, 0x0a, 0x11, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x34, 0x0a, 0x09, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, @@ -1058,101 +1075,108 @@ var file_spire_common_common_proto_rawDesc = []byte{ 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x74, 0x6c, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x74, 0x74, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, - 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x73, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x18, 0x05, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0d, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x73, 0x57, 0x69, - 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, - 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x64, - 0x6d, 0x69, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x77, 0x6e, 0x73, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x6f, 0x77, 0x6e, 0x73, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x45, 0x78, 0x70, 0x69, - 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x45, - 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, - 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x76, 0x69, 0x64, 0x22, 0xd7, 0x02, 0x0a, 0x15, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x4d, 0x61, 0x73, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, - 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, - 0x74, 0x74, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x74, 0x74, 0x6c, 0x12, 0x25, - 0x0a, 0x0e, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x73, 0x5f, 0x77, 0x69, 0x74, 0x68, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x73, 0x57, 0x69, 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, - 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x77, 0x6e, 0x73, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x6f, 0x77, 0x6e, - 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x45, - 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x65, 0x6e, 0x74, - 0x72, 0x79, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x6e, 0x73, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x73, - 0x76, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x53, 0x76, 0x69, 0x64, 0x22, 0x50, 0x0a, 0x13, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x07, 0x65, - 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x73, - 0x70, 0x69, 0x72, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, - 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x2a, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x64, 0x65, 0x72, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x22, 0x59, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, - 0x1d, 0x0a, 0x0a, 0x70, 0x6b, 0x69, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x6b, 0x69, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x69, 0x64, - 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x22, 0xcc, 0x01, - 0x0a, 0x06, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x74, 0x72, 0x75, 0x73, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x49, 0x64, - 0x12, 0x34, 0x0a, 0x08, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x07, 0x72, - 0x6f, 0x6f, 0x74, 0x43, 0x61, 0x73, 0x12, 0x41, 0x0a, 0x10, 0x6a, 0x77, 0x74, 0x5f, 0x73, 0x69, - 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x0e, 0x6a, 0x77, 0x74, 0x53, 0x69, - 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x66, - 0x72, 0x65, 0x73, 0x68, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0b, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x48, 0x69, 0x6e, 0x74, 0x22, 0x74, 0x0a, 0x0a, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, - 0x6f, 0x74, 0x5f, 0x63, 0x61, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x6f, - 0x6f, 0x74, 0x43, 0x61, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6a, 0x77, 0x74, 0x5f, 0x73, 0x69, 0x67, - 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0e, 0x6a, 0x77, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x73, 0x12, - 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x48, 0x69, - 0x6e, 0x74, 0x22, 0x9f, 0x02, 0x0a, 0x10, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4e, - 0x6f, 0x64, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x65, 0x73, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x63, + 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x78, 0x35, 0x30, + 0x39, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x5f, 0x74, 0x74, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0b, 0x78, 0x35, 0x30, 0x39, 0x53, 0x76, 0x69, 0x64, 0x54, 0x74, 0x6c, 0x12, 0x25, 0x0a, + 0x0e, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x73, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x73, + 0x57, 0x69, 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, + 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x77, 0x6e, 0x73, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x6f, 0x77, 0x6e, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x45, 0x78, + 0x70, 0x69, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6e, 0x73, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, + 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x76, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0c, + 0x6a, 0x77, 0x74, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x5f, 0x74, 0x74, 0x6c, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0a, 0x6a, 0x77, 0x74, 0x53, 0x76, 0x69, 0x64, 0x54, 0x74, 0x6c, 0x22, 0x8b, + 0x03, 0x0a, 0x15, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x4d, 0x61, 0x73, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, + 0x12, 0x22, 0x0a, 0x0d, 0x78, 0x35, 0x30, 0x39, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x5f, 0x74, 0x74, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x78, 0x35, 0x30, 0x39, 0x53, 0x76, 0x69, + 0x64, 0x54, 0x74, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, + 0x73, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x66, 0x65, + 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x73, 0x57, 0x69, 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, + 0x64, 0x6f, 0x77, 0x6e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x64, 0x6f, 0x77, 0x6e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x0a, 0x0b, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x1b, + 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x76, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x6a, 0x77, + 0x74, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x5f, 0x74, 0x74, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x6a, 0x77, 0x74, 0x53, 0x76, 0x69, 0x64, 0x54, 0x74, 0x6c, 0x22, 0x50, 0x0a, 0x13, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x72, + 0x69, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x2a, + 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, + 0x09, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x08, 0x64, 0x65, 0x72, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x59, 0x0a, 0x09, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x6b, 0x69, 0x78, 0x5f, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x6b, 0x69, + 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, + 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6e, 0x6f, 0x74, + 0x41, 0x66, 0x74, 0x65, 0x72, 0x22, 0xcc, 0x01, 0x0a, 0x06, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x72, 0x75, 0x73, 0x74, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x75, 0x73, 0x74, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x08, 0x72, 0x6f, 0x6f, 0x74, + 0x5f, 0x63, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x70, 0x69, + 0x72, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x07, 0x72, 0x6f, 0x6f, 0x74, 0x43, 0x61, 0x73, 0x12, 0x41, + 0x0a, 0x10, 0x6a, 0x77, 0x74, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, + 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x52, 0x0e, 0x6a, 0x77, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, + 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x68, 0x69, 0x6e, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, + 0x48, 0x69, 0x6e, 0x74, 0x22, 0x74, 0x0a, 0x0a, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4d, 0x61, + 0x73, 0x6b, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x63, 0x61, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x6f, 0x6f, 0x74, 0x43, 0x61, 0x73, 0x12, 0x28, 0x0a, + 0x10, 0x6a, 0x77, 0x74, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6a, 0x77, 0x74, 0x53, 0x69, 0x67, 0x6e, + 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, + 0x73, 0x68, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x72, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x48, 0x69, 0x6e, 0x74, 0x22, 0x9f, 0x02, 0x0a, 0x10, 0x41, + 0x74, 0x74, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x12, + 0x32, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, + 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, + 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x10, 0x63, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, + 0x72, 0x12, 0x24, 0x0a, 0x0e, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, + 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x4e, + 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x16, 0x6e, 0x65, 0x77, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x63, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0e, 0x63, 0x65, 0x72, - 0x74, 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, - 0x33, 0x0a, 0x16, 0x6e, 0x65, 0x77, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x13, 0x6e, 0x65, 0x77, 0x43, 0x65, 0x72, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, - 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x12, 0x6e, 0x65, 0x77, 0x5f, 0x63, 0x65, 0x72, 0x74, - 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0f, 0x6e, 0x65, 0x77, 0x43, 0x65, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, - 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x61, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x74, 0x74, 0x65, 0x73, - 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x61, 0x74, - 0x74, 0x65, 0x73, 0x74, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x2f, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x6e, 0x65, 0x77, 0x43, 0x65, 0x72, 0x74, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x12, + 0x6e, 0x65, 0x77, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, + 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6e, 0x65, 0x77, 0x43, 0x65, 0x72, + 0x74, 0x4e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x61, 0x6e, + 0x5f, 0x72, 0x65, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0b, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x42, 0x2c, 0x5a, 0x2a, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x69, 0x66, 0x66, + 0x65, 0x2f, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x70, + 0x69, 0x72, 0x65, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/proto/spire/common/common.proto b/proto/spire/common/common.proto index 057698b85d..f42573a127 100644 --- a/proto/spire/common/common.proto +++ b/proto/spire/common/common.proto @@ -68,8 +68,8 @@ message RegistrationEntry { caller. It is defined as a URI comprising a “trust domain” and an associated path. */ string spiffe_id = 3; - /** Time to live. */ - int32 ttl = 4; + /** Time to live for X509-SVIDs generated from this entry. Was previously called 'ttl'. */ + int32 x509_svid_ttl = 4; /** A list of federated trust domain SPIFFE IDs. */ repeated string federates_with = 5; /** Entry ID */ @@ -88,6 +88,8 @@ message RegistrationEntry { int64 revision_number = 11; /** Determines if the issued SVID must be stored through an SVIDStore plugin */ bool store_svid = 12; + /** Time to live for JWT-SVIDs generated from this entry, if set will override ttl field. */ + int32 jwt_svid_ttl = 13; } /** The RegistrationEntryMask is used to update only selected fields of the RegistrationEntry */ @@ -95,7 +97,7 @@ message RegistrationEntryMask { bool selectors = 1; bool parent_id = 2; bool spiffe_id = 3; - bool ttl = 4; + bool x509_svid_ttl = 4; bool federates_with = 5; bool entry_id = 6; bool admin = 7; @@ -103,6 +105,7 @@ message RegistrationEntryMask { bool entryExpiry = 9; bool dns_names = 10; bool store_svid = 11; + bool jwt_svid_ttl = 12; } diff --git a/release/posix/spire-extras/README.md b/release/posix/spire-extras/README.md index b9f629e7d2..096d924cf8 100644 --- a/release/posix/spire-extras/README.md +++ b/release/posix/spire-extras/README.md @@ -1,4 +1,4 @@ -= SPIRE Extras +# SPIRE Extras - [SPIRE Kubernetes Workload Registrar](https://github.com/spiffe/spire/blob/main/support/k8s/k8s-workload-registrar/README.md) - [SPIRE OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md) @@ -6,7 +6,7 @@ The configuration files included in this release are intended for evaluation purposes only and are **NOT** production ready. -== Contents +## Contents | Path | Description | |-------------------------------------------------------------|----------------------------------------------------------| diff --git a/release/posix/spire/README.md b/release/posix/spire/README.md index 2a65eb2d0b..cfb510f100 100644 --- a/release/posix/spire/README.md +++ b/release/posix/spire/README.md @@ -1,13 +1,13 @@ -= SPIRE +# SPIRE -[SPIRE](https://github.com/spiffe/spire) (the [SPIFFE](https://github.com/spiffe/spiffe) Runtime Environment) is a tool-chain for establishing trust between software systems across a wide variety of hosting platforms. +[SPIRE](https://github.com/spiffe/spire) (the [SPIFFE](https://github.com/spiffe/spiffe) Runtime Environment) is a tool-chain for establishing trust between software systems across a wide variety of hosting platforms. The configuration files included in this release are intended for evaluation purposes only and are **NOT** production ready. You can find additional example configurations for SPIRE [here](https://github.com/spiffe/spire-examples). -== Contents +## Contents | Path | Description | |---------------------------|-----------------------------------| diff --git a/release/posix/spire/conf/server/server.conf b/release/posix/spire/conf/server/server.conf index 65eb750e17..5f651732d0 100644 --- a/release/posix/spire/conf/server/server.conf +++ b/release/posix/spire/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "./data/server" log_level = "DEBUG" ca_ttl = "168h" - default_svid_ttl = "48h" + default_x509_svid_ttl = "48h" } plugins { diff --git a/release/windows/spire-extras/README.md b/release/windows/spire-extras/README.md index d2b40af276..aa5d799278 100644 --- a/release/windows/spire-extras/README.md +++ b/release/windows/spire-extras/README.md @@ -1,11 +1,11 @@ -= SPIRE Extras +# SPIRE Extras - [SPIRE OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md) The configuration files included in this release are intended for evaluation purposes only and are **NOT** production ready. -== Contents +## Contents | Path | Description | |-------------------------------------------------------------|----------------------------------------------------| diff --git a/release/windows/spire/README.md b/release/windows/spire/README.md index 7b6018de5c..09a4a87c4f 100644 --- a/release/windows/spire/README.md +++ b/release/windows/spire/README.md @@ -1,13 +1,13 @@ -= SPIRE +# SPIRE -[SPIRE](https://github.com/spiffe/spire) (the [SPIFFE](https://github.com/spiffe/spiffe) Runtime Environment) is a tool-chain for establishing trust between software systems across a wide variety of hosting platforms. +[SPIRE](https://github.com/spiffe/spire) (the [SPIFFE](https://github.com/spiffe/spiffe) Runtime Environment) is a tool-chain for establishing trust between software systems across a wide variety of hosting platforms. The configuration files included in this release are intended for evaluation purposes only and are **NOT** production ready. You can find additional example configurations for SPIRE [here](https://github.com/spiffe/spire-examples). -== Contents +## Contents | Path | Description | |---------------------------|-----------------------------------| diff --git a/release/windows/spire/conf/server/server.conf b/release/windows/spire/conf/server/server.conf index 1e0222de7b..d52efcec34 100644 --- a/release/windows/spire/conf/server/server.conf +++ b/release/windows/spire/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "./data/server" log_level = "DEBUG" ca_ttl = "168h" - default_svid_ttl = "48h" + default_x509_svid_ttl = "48h" } plugins { diff --git a/support/k8s/k8s-workload-registrar/README.md b/support/k8s/k8s-workload-registrar/README.md index da6b0d756f..aeeac1263c 100644 --- a/support/k8s/k8s-workload-registrar/README.md +++ b/support/k8s/k8s-workload-registrar/README.md @@ -1,5 +1,7 @@ # SPIRE Kubernetes Workload Registrar +**The SPIRE Kubernetes Workload Registrar is deprecated and no longer maintained. Please migrate to the [SPIRE Controller Manager](https://github.com/spiffe/spire-controller-manager).** + The SPIRE Kubernetes Workload Registrar implements a Kubernetes ValidatingAdmissionWebhook that facilitates automatic workload registration within Kubernetes. @@ -14,7 +16,6 @@ The registrar has the following command line flags: |-----------|------------------------------------------------------------------|-------------------------------| | `-config` | Path on disk to the [HCL Configuration](#hcl-configuration) file | `k8s-workload-registrar.conf` | - ### HCL Configuration The configuration file is a **required** by the registrar. It contains @@ -32,7 +33,7 @@ The configuration file is a **required** by the registrar. It contains | `pod_label` | string | optional | The pod label used for [Label Based Workload Registration](#label-based-workload-registration) | | | `pod_annotation` | string | optional | The pod annotation used for [Annotation Based Workload Registration](#annotation-based-workload-registration) | | | `mode` | string | required | How to run the registrar, either `"reconcile"` or `"crd"`. See [Differences](#differences-between-modes) for more details. | | -| `disabled_namespaces` | []string | optional | Comma seperated list of namespaces to disable auto SVID generation for | `"kube-system", "kube-public"` | +| `disabled_namespaces` | []string | optional | Comma separated list of namespaces to disable auto SVID generation for | `"kube-system", "kube-public"` | The following configuration directives are specific to `"reconcile"` mode: @@ -49,7 +50,7 @@ For CRD configuration directives see [CRD Mode Configuration](mode-crd/README.md ### Example -``` +```hcl log_level = "debug" trust_domain = "domain.test" server_socket_path = "/tmp/spire-server/private/api.sock" @@ -57,6 +58,7 @@ cluster = "production" ``` ## Workload Registration + When running in reconcile or crd mode with `pod_controller=true` entries will be automatically created for Pods. The available workload registration modes are: @@ -72,12 +74,11 @@ workload registration mode is selected, `identity_template` is used with a default configuration: `ns/{{.Pod.Namespace}}/sa/{{.Pod.ServiceAccount}}` - It may take several seconds for newly created SVIDs to become available to workloads. ### Federated Entry Registration -The pod annotatation `spiffe.io/federatesWith` can be used to create SPIFFE ID's that federate with other trust domains. +The pod annotation `spiffe.io/federatesWith` can be used to create SPIFFE ID's that federate with other trust domains. To specify multiple trust domains, separate them with commas. @@ -106,7 +107,7 @@ SPIFFE ID of the form pod came in with the service account `blog` in the `production` namespace, the following registration entry would be created: -``` +```shell Entry ID : 200d8b19-8334-443d-9494-f65d0ad64eb5 SPIFFE ID : spiffe://example.org/ns/production/sa/blog Parent ID : ... @@ -123,7 +124,7 @@ was configured with the `spire-workload` label and a pod came in with `spire-workload=example-workload`, the following registration entry would be created: -``` +```shell Entry ID : 200d8b19-8334-443d-9494-f65d0ad64eb5 SPIFFE ID : spiffe://example.org/example-workload Parent ID : ... @@ -143,7 +144,7 @@ was configured with the `spiffe.io/spiffe-id` annotation and a pod came in with `spiffe.io/spiffe-id: production/example-workload`, the following registration entry would be created: -``` +```shell Entry ID : 200d8b19-8334-443d-9494-f65d0ad64eb5 SPIFFE ID : spiffe://example.org/production/example-workload Parent ID : ... @@ -167,11 +168,10 @@ the registrar deployment. If it is deployed as a container within the SPIRE server pod then it talks to SPIRE server via a Unix domain socket. It will need access to a shared volume containing the socket file. - ### Reconcile Mode Configuration To use reconcile mode you need to create appropriate roles and bind them to the ServiceAccount you intend to run the controller as. -An example can be found in `mode-reconcile/config/role.yaml`, which you would apply with `kubectl apply -f mode-reconcile/config/role.yaml` +An example can be found in `mode-reconcile/config/roles.yaml`, which you would apply with `kubectl apply -f mode-reconcile/config/role.yaml` ### CRD Mode Configuration diff --git a/support/k8s/k8s-workload-registrar/config_crd.go b/support/k8s/k8s-workload-registrar/config_crd.go index f72a993ab3..77b56d4c5f 100644 --- a/support/k8s/k8s-workload-registrar/config_crd.go +++ b/support/k8s/k8s-workload-registrar/config_crd.go @@ -105,6 +105,8 @@ func (c *CRDMode) Run(ctx context.Context) error { } defer log.Close() + log.Warn("The k8s-workload-registrar is deprecated and no longer maintained. Please migrate to the SPIRE Controller Manager (https://github.com/spiffe/spire-controller-manager).") + // DEPRECATED: remove this check in 1.5.0 since all those who migrate through 1.4.0 will already have moved away if c.LeaderElection && c.LeaderElectionResourceLock == configMapsResourceLock { return errs.New(`the "configmaps" leader election resource lock type is no longer supported`) diff --git a/support/k8s/k8s-workload-registrar/config_reconcile.go b/support/k8s/k8s-workload-registrar/config_reconcile.go index 3f29e0128e..f009a29e37 100644 --- a/support/k8s/k8s-workload-registrar/config_reconcile.go +++ b/support/k8s/k8s-workload-registrar/config_reconcile.go @@ -69,6 +69,8 @@ func (c *ReconcileMode) Run(ctx context.Context) error { })) setupLog := ctrl.Log.WithName("setup") + setupLog.Info("The k8s-workload-registrar is deprecated and no longer maintained. Please migrate to the SPIRE Controller Manager (https://github.com/spiffe/spire-controller-manager).") + // DEPRECATED: remove this check in 1.5.0 since all those who migrate through 1.4.0 will already have moved away if c.LeaderElection && c.LeaderElectionResourceLock == configMapsResourceLock { return errs.New(`the "configmaps" leader election resource lock type is no longer supported`) diff --git a/support/k8s/k8s-workload-registrar/mode-crd/README.md b/support/k8s/k8s-workload-registrar/mode-crd/README.md index f688095136..223c8d63d0 100644 --- a/support/k8s/k8s-workload-registrar/mode-crd/README.md +++ b/support/k8s/k8s-workload-registrar/mode-crd/README.md @@ -1,15 +1,17 @@ # SPIRE Kubernetes Workload Registrar (CRD Mode) +**The SPIRE Kubernetes Workload Registrar is deprecated and no longer maintained. Please migrate to the [SPIRE Controller Manager](https://github.com/spiffe/spire-controller-manager).** + The CRD mode of the SPIRE Kubernetes Workload Registrar uses a Kubernetes Custom Resource Definition (CRD) to integrate SPIRE and Kubernetes. This enables auto and manual generation of SPIFFE IDs from with Kubernetes and the `kubectl` CLI. ## Benefits of CRD Kubernetes Workload Registrar -There are mutiple modes of the Kubernetes Workload Registrar. The benefits of the CRD mode when compared to other modes are: +There are multiple modes of the Kubernetes Workload Registrar. The benefits of the CRD mode when compared to other modes are: -* **`kubectl` integration**: Using a CRD, SPIRE is fully intergrated with Kubernetes. You can view and create SPIFFE IDs directly using `kubectl`, without having to shell into the SPIRE server. -* **Fully event-driven design**: Using the Kubernetes CRD system, the CRD mode Kubernetes Workload Registrar is fully event-driven to minimze resource usage. -* **Standards-based solution**: CRDs are the standard way to extend Kubernetes, with many resources online, such as [kubebuilder](https://book.kubebuilder.io/), discussing the approach. The CRD Kubernetes Worklaod Registrar follows all standards and best practices to ensure it is maintainable. +* **`kubectl` integration**: Using a CRD, SPIRE is fully integrated with Kubernetes. You can view and create SPIFFE IDs directly using `kubectl`, without having to shell into the SPIRE server. +* **Fully event-driven design**: Using the Kubernetes CRD system, the CRD mode Kubernetes Workload Registrar is fully event-driven to minimize resource usage. +* **Standards-based solution**: CRDs are the standard way to extend Kubernetes, with many resources online, such as [kubebuilder](https://book.kubebuilder.io/), discussing the approach. The CRD Kubernetes Workload Registrar follows all standards and best practices to ensure it is maintainable. ## Configuration @@ -21,7 +23,6 @@ The registrar has the following command line flags: |-----------|------------------------------------------------------------------|-------------------------------| | `-config` | Path on disk to the [HCL Configuration](#hcl-configuration) file | `k8s-workload-registrar.conf` | - ### HCL Configuration The configuration file is a **required** by the registrar. It contains @@ -49,7 +50,7 @@ The configuration file is a **required** by the registrar. It contains | `server_address` | string | required | Address of the spire server. A local socket can be specified using unix:///path/to/socket. This is not the same as the agent socket. | | | `server_socket_path` | string | optional | Path to the Unix domain socket of the SPIRE server, equivalent to specifying a server_address with a "unix://..." prefix | | | `trust_domain` | string | required | Trust domain of the SPIRE server | | -| `webhook_enabled` | bool | optional | Enable a validating webhook to ensure CRDs are properly fomatted and there are no duplicates. | `false` | +| `webhook_enabled` | bool | optional | Enable a validating webhook to ensure CRDs are properly formatted and there are no duplicates. | `false` | | `webhook_port` | int | optional | The port to use for the validating webhook. | `9443` | | `webhook_service_name` | string | optional | The name of the Kubernetes Service being used for the webhook. | `"k8s-workload-registrar"` | @@ -58,14 +59,17 @@ The configuration file is a **required** by the registrar. It contains This quick start sets up the SPIRE Server, SPIRE Agent, and CRD Kubernetes Workload Registrar. 1. Deploy SPIRE Server, Kubernetes Workload Registrar, SPIRE Agent, and CRD. SPIRE Server and Kubernetes Workload Registrar will be deployed in the same Pod. - ``` - kubectl apply -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spiffeid.spiffe.io_spiffeids.yaml \ + + ```shell + $ kubectl apply -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spiffeid.spiffe.io_spiffeids.yaml \ + -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/roles.yaml \ -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spire-server-registrar.yaml \ -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spire-agent.yaml ``` 1. Verify the deployment succeeded. - ``` + + ```shell $ kubectl get pods -n spire NAME READY STATUS RESTARTS AGE spire-agent-4wdxx 1/1 Running 0 5m59s @@ -83,12 +87,14 @@ Here are some examples of things you can do once the CRD Kubernetes Workload Reg ### Create a SpiffeID Resource using kubectl 1. Create a SpiffeID resource. - ``` - kubectl apply -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/samples/test_spiffeid.yaml + + ```shell + $ kubectl apply -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/samples/test_spiffeid.yaml ``` 1. Check that the SpiffeID resource was created. - ``` + + ```shell $ kubectl get spiffeids NAME AGE my-test-spiffeid 85s @@ -119,7 +125,8 @@ Here are some examples of things you can do once the CRD Kubernetes Workload Reg ``` 1. Verify the SPIFFE ID was created on the SPIRE Server - ``` + + ```shell $ kubectl exec spire-server-0 -n spire -c spire-server -- ./bin/spire-server entry show -spiffeID spiffe://example.org/test Found 1 entry Entry ID : ad49519e-37a1-4de5-a661-c091d3652b9c @@ -132,14 +139,16 @@ Here are some examples of things you can do once the CRD Kubernetes Workload Reg ``` 1. Delete the SpiffeID resource, the corresponding entry on the SPIRE Server will be removed. - ``` - kubectl delete -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/samples/test_spiffeid.yaml + + ```shell + $ kubectl delete -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/samples/test_spiffeid.yaml ``` ### Attempt to Deploy an Invalid SpiffeID Resource 1. Apply deploy an invalid SpiffeID. - ``` + + ```shell $ kubectl apply -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/samples/test_spiffeid_bad.yaml Error from server (spec.Selector.Namespace must match namespace of resource): error when creating "test_spiffeid_bad.yaml": admission webhook "k8s-workload-registrar.nginx-mesh.svc" denied the request: spec.Selector.Namespace must match namespace of resource ``` @@ -150,24 +159,27 @@ Here are some examples of things you can do once the CRD Kubernetes Workload Reg To test auto-generation of SPIFFE IDs add the following label to a Pod Spec and then apply it. The format for the auto-generated SPIFFE ID in this example is `ns//pod/`. - ``` + ```yaml spiffe.io/spiffe-id: true ``` We can test this using the NGINX example deployment: 1. Deploy the example NGINX deployment - ``` - kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/application/simple_deployment.yaml + + ```shell + $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/application/simple_deployment.yaml ``` 1. Add the label to the Deployment Template. This will reroll the deployment - ``` - kubectl patch deployment nginx-deployment -p '{"spec":{"template":{"metadata":{"labels":{"spiffe.io/spiffe-id": "true"}}}}}' + + ```shell + $ kubectl patch deployment nginx-deployment -p '{"spec":{"template":{"metadata":{"labels":{"spiffe.io/spiffe-id": "true"}}}}}' ``` 1. Verify the SpiffeID resource was created. The name of the SpiffeID resource will be the same as the name of the Pod. - ``` + + ```shell $ kubectl get spiffeids NAME AGE nginx-deployment-7ffbd8bd54-rcnt8 4s @@ -244,21 +256,25 @@ We can test this using the NGINX example deployment: ``` 1. Delete the NGINX deployment, this will automatically delete the SpiffeID resource - ``` - kubectl delete -f https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/application/simple_deployment.yaml + + ```shell + $ kubectl delete -f https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/application/simple_deployment.yaml ``` ## Deleting the Quick Start 1. Delete the CRD. This needs to be done before remove the Kubernetes Workload Registrar to give the finalizers a chance to complete. - ``` - kubectl delete -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spiffeid.spiffe.io_spiffeids.yaml + + ```shell + $ kubectl delete -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spiffeid.spiffe.io_spiffeids.yaml ``` 1. Delete the remaining previously applied yaml files. - ``` - kubectl delete -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spire-server-registrar.yaml \ - -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spire-agent.yaml + + ```shell + $ kubectl delete -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spire-server-registrar.yaml \ + -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/spire-agent.yaml \ + -f https://raw.githubusercontent.com/spiffe/spire/main/support/k8s/k8s-workload-registrar/mode-crd/config/roles.yaml ``` ## Workload Registration @@ -278,6 +294,7 @@ The template formatter is using Golang [text/template](https://pkg.go.dev/text/template) conventions, and it can reference arbitrary values provided in the `context` map of strings in addition to the following Pod-specific arguments: + * Pod.Name * Pod.UID * Pod.Namespace @@ -286,15 +303,18 @@ in addition to the following Pod-specific arguments: * Pod.NodeName For example if the registrar was configured with the following: -``` + +```hcl identity_template = "region/{{.Context.Region}}/cluster/{{.Context.ClusterName}}/sa/{{.Pod.ServiceAccount}}/pod_name/{{.Pod.Name}}" context { Region = "US-NORTH" ClusterName = "MYCLUSTER" } ``` + and the _example-workload_ pod was deployed in _production_ namespace and _myserviceacct_ service account, the following registration entry would be created: -``` + +```shell Entry ID : 200d8b19-8334-443d-9494-f65d0ad64eb5 SPIFFE ID : spiffe://example.org/region/US-NORTH/cluster/MYCLUSTER/sa/myserviceacct/pod_name/example-workload Parent ID : ... @@ -305,7 +325,7 @@ Selector : k8s:pod-name:example-workload-98b6b79fd-jnv5m If `identity_template_label` is defined in the registrar configuration: -``` +```hcl identity_template_label = "enable_identity_template" ``` @@ -321,6 +341,7 @@ spec: containers: ... ``` + Pods that don't contain the pod label are ignored. If `identity_template_label` is empty or omitted, all the pods will receive the identity. @@ -333,7 +354,7 @@ was configured with the `spire-workload` label and a pod came in with `spire-workload=example-workload`, the following registration entry would be created: -``` +```shell Entry ID : 200d8b19-8334-443d-9494-f65d0ad64eb5 SPIFFE ID : spiffe://example.org/example-workload Parent ID : ... @@ -353,7 +374,7 @@ was configured with the `spiffe.io/spiffe-id` annotation and a pod came in with `spiffe.io/spiffe-id: production/example-workload`, the following registration entry would be created: -``` +```shell Entry ID : 200d8b19-8334-443d-9494-f65d0ad64eb5 SPIFFE ID : spiffe://example.org/production/example-workload Parent ID : ... @@ -371,7 +392,7 @@ The default SPIFFE ID created with [Identity Template Based Workload Registratio ### Federated Entry Registration -The pod annotatation `spiffe.io/federatesWith` can be used to create SPIFFE ID's that federate with other trust domains. +The pod annotation `spiffe.io/federatesWith` can be used to create SPIFFE ID's that federate with other trust domains. To specify multiple trust domains, separate them with commas. @@ -393,6 +414,7 @@ spec: If DNS names are desired for your workload, they can be specified using the `dns_name_templates` configuration option. Similar to the `identity_template` field, `dns_name_templates` uses Golang [text/template](https://pkg.go.dev/text/template) conventions. It can reference arbitrary values provided in the `context` map of strings, in addition to the following Pod-specific arguments: + * Pod.Name * Pod.UID * Pod.Namespace @@ -403,18 +425,20 @@ If DNS names are desired for your workload, they can be specified using the `dns `dns_name_templates` is a list of strings, and gets added to the `dnsNames` list in the SpiffeID CRD. For example if the registrar was configured with the following: -``` + +```hcl dns_name_templates = ["{{.Pod.ServiceAccount}}.{{.Pod.Namespace}}.svc", "{{.Context.Domain}}.{{.Pod.Name}}.svc"] context { Domain = "my-domain" } ``` + and the _example-workload_ pod was deployed in _production_ namespace and _myserviceacct_ service account, the following DNS names will be added to the SpiffeID CRD: -- myserviceacct.production.svc -- my-domain.example-workload.svc +* myserviceacct.production.svc +* my-domain.example-workload.svc -
Note: The first template in the list will also populate the Common Name (CN) field of the SVID.
+_Note: The first template in the list will also populate the Common Name (CN) field of the SVID._ ## How it Works @@ -440,9 +464,10 @@ A Validating Webhook is used to ensure SpiffeID resources are properly formatted The certificates for the webhook are generated by the SPIRE Server and managed by the Kubernetes Workload Registrar. ## SPIFFE ID Custom Resource Example + An example SPIFFE ID custom resource is below: -``` +```yaml apiVersion: spiffeid.spiffe.io/v1beta1 kind: SpiffeID metadata: @@ -462,22 +487,25 @@ spec: ``` The supported selectors are: -- arbitrary -- Arbitrary selectors -- containerName -- Name of the container -- containerImage -- Container image used -- namespace -- Namespace to match for this SPIFFE ID -- nodeName -- Node name to match for this SPIFFE ID -- podLabel -- Pod label name/value to match for this SPIFFE ID -- podName -- Pod name to match for this SPIFFE ID -- podUID -- Pod UID to match for this SPIFFE ID -- serviceAccount -- ServiceAccount to match for this SPIFFE ID - -Notes: + +* arbitrary -- Arbitrary selectors +* containerName -- Name of the container +* containerImage -- Container image used +* namespace -- Namespace to match for this SPIFFE ID +* nodeName -- Node name to match for this SPIFFE ID +* podLabel -- Pod label name/value to match for this SPIFFE ID +* podName -- Pod name to match for this SPIFFE ID +* podUID -- Pod UID to match for this SPIFFE ID +* serviceAccount -- ServiceAccount to match for this SPIFFE ID + +Notes: + * Specifying DNS Names is optional * Specifying downstream is optional * The metadata.namespace and selector.namespace must match ## CRD Security Considerations + It is imperative to only grant trusted users access to manually create SpiffeID custom resources. Users with access have the ability to issue any SpiffeID to any pod in the namespace. @@ -492,6 +520,6 @@ entries can only be consumed by workloads within that namespace. The k8s ValidatingWebhookConfiguration will need to be removed or pods may fail admission. If you used the default configuration this can be done with: -``` -kubectl validatingwebhookconfiguration delete k8s-workload-registrar-webhook +```shell +$ kubectl validatingwebhookconfiguration delete k8s-workload-registrar-webhook ``` diff --git a/support/k8s/k8s-workload-registrar/mode-crd/config/roles.yaml b/support/k8s/k8s-workload-registrar/mode-crd/config/roles.yaml new file mode 100644 index 0000000000..6639030d40 --- /dev/null +++ b/support/k8s/k8s-workload-registrar/mode-crd/config/roles.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: spire-k8s-registrar-cluster-role +rules: + - apiGroups: [""] + resources: ["pods", "nodes", "endpoints"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: spire-k8s-registrar-cluster-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: spire-k8s-registrar-cluster-role +subjects: + - kind: ServiceAccount + name: spire-k8s-registrar + namespace: spire +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: spire-k8s-registrar-role + namespace: spire +rules: + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["create"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + resourceNames: ["spire-k8s-registrar-leader-election"] + verbs: ["update", "get"] + - apiGroups: [""] + resources: ["events"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: spire-k8s-registrar-role-binding + namespace: spire +subjects: + - kind: ServiceAccount + name: spire-k8s-registrar + namespace: spire +roleRef: + kind: Role + name: spire-k8s-registrar-role + apiGroup: rbac.authorization.k8s.io diff --git a/support/k8s/k8s-workload-registrar/mode-crd/config/spire-agent.yaml b/support/k8s/k8s-workload-registrar/mode-crd/config/spire-agent.yaml index b6885df42a..29ce4a4b22 100644 --- a/support/k8s/k8s-workload-registrar/mode-crd/config/spire-agent.yaml +++ b/support/k8s/k8s-workload-registrar/mode-crd/config/spire-agent.yaml @@ -108,16 +108,9 @@ spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet serviceAccountName: spire-agent - initContainers: - - name: init - # This is a small image with wait-for-it, choose whatever image - # you prefer that waits for a service to be up. This image is built - # from https://github.com/lqhl/wait-for-it - image: gcr.io/spiffe-io/wait-for-it - args: ["-t", "30", "spire-server:8081"] containers: - name: spire-agent - image: gcr.io/spiffe-io/spire-agent:1.3.0 + image: ghcr.io/spiffe/spire-agent:1.5.1 args: ["-config", "/run/spire/config/agent.conf"] volumeMounts: - name: spire-config diff --git a/support/k8s/k8s-workload-registrar/mode-crd/config/spire-server-registrar.yaml b/support/k8s/k8s-workload-registrar/mode-crd/config/spire-server-registrar.yaml index f7f38ff063..fdc2d330fc 100644 --- a/support/k8s/k8s-workload-registrar/mode-crd/config/spire-server-registrar.yaml +++ b/support/k8s/k8s-workload-registrar/mode-crd/config/spire-server-registrar.yaml @@ -71,7 +71,7 @@ data: trust_domain = "example.org" data_dir = "/run/spire/data" log_level = "DEBUG" - default_svid_ttl = "1h" + default_x509_svid_ttl = "1h" ca_subject = { country = ["US"], organization = ["SPIFFE"], @@ -158,6 +158,7 @@ data: pod_controller = true add_svc_dns_names = true mode = "crd" + leader_election = true webhook_enabled = true identity_template = "ns/{{.Pod.Namespace}}/pod/{{.Pod.Name}}" identity_template_label = "spiffe.io/spiffe-id" @@ -211,7 +212,7 @@ spec: shareProcessNamespace: true containers: - name: spire-server - image: gcr.io/spiffe-io/spire-server:1.3.0 + image: ghcr.io/spiffe/spire-server:1.5.1 args: - -config - /run/spire/config/server.conf @@ -240,7 +241,7 @@ spec: mountPath: /tmp readOnly: false - name: k8s-workload-registrar - image: gcr.io/spiffe-io/k8s-workload-registrar:1.3.0 + image: gcr.io/spiffe-io/k8s-workload-registrar:1.5.1 args: - -config - /run/spire/config/k8s-workload-registrar.conf diff --git a/support/k8s/k8s-workload-registrar/mode-crd/controllers/utils.go b/support/k8s/k8s-workload-registrar/mode-crd/controllers/utils.go index 66b7bf6763..804c539625 100644 --- a/support/k8s/k8s-workload-registrar/mode-crd/controllers/utils.go +++ b/support/k8s/k8s-workload-registrar/mode-crd/controllers/utils.go @@ -77,7 +77,7 @@ func setOwnerRef(owner metav1.Object, spiffeID *spiffeidv1beta1.SpiffeID, scheme if ownerRef == nil { return err } - ownerRef.BlockOwnerDeletion = pointer.BoolPtr(false) + ownerRef.BlockOwnerDeletion = pointer.Bool(false) return nil } diff --git a/support/k8s/k8s-workload-registrar/mode-crd/webhook/webhook_svid.go b/support/k8s/k8s-workload-registrar/mode-crd/webhook/webhook_svid.go index ed0481e1e5..5f94654d65 100644 --- a/support/k8s/k8s-workload-registrar/mode-crd/webhook/webhook_svid.go +++ b/support/k8s/k8s-workload-registrar/mode-crd/webhook/webhook_svid.go @@ -21,14 +21,13 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1" spiretypes "github.com/spiffe/spire-api-sdk/proto/spire/api/types" + "github.com/spiffe/spire/pkg/common/diskutil" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" ) const ( certDirMode = os.FileMode(0o700) - certsFileMode = os.FileMode(0o644) - keyFileMode = os.FileMode(0o600) certsFileName = "tls.crt" keyFileName = "tls.key" ) @@ -181,13 +180,13 @@ func (e *SVID) dumpSVID(svid *spiretypes.X509SVID, key crypto.Signer) error { // Write certificates to disk certsFileName := path.Join(e.c.WebhookCertDir, certsFileName) - if err := os.WriteFile(certsFileName, svidPEM.Bytes(), certsFileMode); err != nil { + if err := diskutil.WritePubliclyReadableFile(certsFileName, svidPEM.Bytes()); err != nil { return err } // Write key to disk keyFileName := path.Join(e.c.WebhookCertDir, keyFileName) - return os.WriteFile(keyFileName, keyPEM.Bytes(), keyFileMode) + return diskutil.WritePrivateFile(keyFileName, keyPEM.Bytes()) } func certHalfLife(cert *x509.Certificate) time.Time { diff --git a/support/k8s/k8s-workload-registrar/mode-reconcile/config/roles.yaml b/support/k8s/k8s-workload-registrar/mode-reconcile/config/roles.yaml index 2c4153dfd2..6639030d40 100644 --- a/support/k8s/k8s-workload-registrar/mode-reconcile/config/roles.yaml +++ b/support/k8s/k8s-workload-registrar/mode-reconcile/config/roles.yaml @@ -27,12 +27,12 @@ metadata: name: spire-k8s-registrar-role namespace: spire rules: - - apiGroups: [""] + - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["create"] - - apiGroups: [""] + - apiGroups: ["coordination.k8s.io"] resources: ["leases"] - resourceNames: ["controller-leader-election-helper"] + resourceNames: ["spire-k8s-registrar-leader-election"] verbs: ["update", "get"] - apiGroups: [""] resources: ["events"] diff --git a/support/k8s/k8s-workload-registrar/mode-reconcile/controllers/pod_controller_test.go b/support/k8s/k8s-workload-registrar/mode-reconcile/controllers/pod_controller_test.go index 600b630526..eb767cd3f7 100644 --- a/support/k8s/k8s-workload-registrar/mode-reconcile/controllers/pod_controller_test.go +++ b/support/k8s/k8s-workload-registrar/mode-reconcile/controllers/pod_controller_test.go @@ -39,8 +39,6 @@ type PodControllerTestSuite struct { ds *fakedatastore.DataStore entryClient *fakeentryclient.Client - k8sClient client.Client - log logr.Logger } @@ -52,8 +50,6 @@ func (s *PodControllerTestSuite) SetupTest() { s.ctrl = mockCtrl - s.k8sClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() - s.log = zap.New() } @@ -85,8 +81,9 @@ func (s *PodControllerTestSuite) TestAddChangeRemovePod() { for _, tt := range tests { tt := tt s.Run(tt.first, func() { + k8sClient := createK8sClient() r := NewPodReconciler( - s.k8sClient, + k8sClient, s.log, scheme.Scheme, podControllerTestTrustDomain, @@ -122,7 +119,7 @@ func (s *PodControllerTestSuite) TestAddChangeRemovePod() { _, err := s.ds.AppendBundle(ctx, &common.Bundle{TrustDomainId: "spiffe://example.io"}) s.Assert().NoError(err) - err = s.k8sClient.Create(ctx, &pod) + err = k8sClient.Create(ctx, &pod) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -143,7 +140,7 @@ func (s *PodControllerTestSuite) TestAddChangeRemovePod() { pod.Annotations["spiffe"] = "annotation2" pod.Spec.ServiceAccountName = "sa2" - err = s.k8sClient.Update(ctx, &pod) + err = k8sClient.Update(ctx, &pod) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -166,7 +163,7 @@ func (s *PodControllerTestSuite) TestAddChangeRemovePod() { s.Assert().NoError(err) s.Assert().Len(es, 1) - err = s.k8sClient.Delete(ctx, &pod) + err = k8sClient.Delete(ctx, &pod) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -189,8 +186,32 @@ func (s *PodControllerTestSuite) TestAddChangeRemovePod() { func (s *PodControllerTestSuite) TestAddDnsNames() { ctx := context.TODO() + endpointsToCreate := corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{Name: "foo-svc", Namespace: "bar"}, + Subsets: []corev1.EndpointSubset{{ + Addresses: []corev1.EndpointAddress{ + { + IP: "123.123.123.123", + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Namespace: "bar", + Name: "foo", + }, + }, + }, + Ports: []corev1.EndpointPort{ + { + Name: "endpointName", + Protocol: "TCP", + Port: 12345, + }, + }, + }}, + } + k8sClient := createK8sClientWithEndpoint(&endpointsToCreate, "foo") + r := NewPodReconciler( - s.k8sClient, + k8sClient, s.log, scheme.Scheme, podControllerTestTrustDomain, @@ -218,7 +239,7 @@ func (s *PodControllerTestSuite) TestAddDnsNames() { PodIP: "123.123.123.124", }, } - err := s.k8sClient.Create(ctx, &pod) + err := k8sClient.Create(ctx, &pod) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -240,30 +261,7 @@ func (s *PodControllerTestSuite) TestAddDnsNames() { }, es[0].DnsNames) } - endpointsToCreate := corev1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{Name: "foo-svc", Namespace: "bar"}, - Subsets: []corev1.EndpointSubset{{ - Addresses: []corev1.EndpointAddress{ - { - IP: "123.123.123.123", - TargetRef: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "bar", - Name: "foo", - }, - }, - }, - Ports: []corev1.EndpointPort{ - { - Name: "endpointName", - Protocol: "TCP", - Port: 12345, - }, - }, - }}, - } - - err = s.k8sClient.Create(ctx, &endpointsToCreate) + err = k8sClient.Create(ctx, &endpointsToCreate) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -302,8 +300,32 @@ func (s *PodControllerTestSuite) TestAddDnsNames() { func (s *PodControllerTestSuite) TestDottedPodNamesDns() { ctx := context.TODO() + endpointsToCreate := corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{Name: "foo-svc", Namespace: "bar"}, + Subsets: []corev1.EndpointSubset{{ + Addresses: []corev1.EndpointAddress{ + { + IP: "123.123.123.123", + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Namespace: "bar", + Name: "foo.3.0.0.woo", + }, + }, + }, + Ports: []corev1.EndpointPort{ + { + Name: "endpointName", + Protocol: "TCP", + Port: 12345, + }, + }, + }}, + } + k8sClient := createK8sClientWithEndpoint(&endpointsToCreate, "foo.3.0.0.woo") + r := NewPodReconciler( - s.k8sClient, + k8sClient, s.log, scheme.Scheme, podControllerTestTrustDomain, @@ -331,33 +353,10 @@ func (s *PodControllerTestSuite) TestDottedPodNamesDns() { PodIP: "123.123.123.124", }, } - err := s.k8sClient.Create(ctx, &pod) + err := k8sClient.Create(ctx, &pod) s.Assert().NoError(err) - endpointsToCreate := corev1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{Name: "foo-svc", Namespace: "bar"}, - Subsets: []corev1.EndpointSubset{{ - Addresses: []corev1.EndpointAddress{ - { - IP: "123.123.123.123", - TargetRef: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "bar", - Name: "foo.3.0.0.woo", - }, - }, - }, - Ports: []corev1.EndpointPort{ - { - Name: "endpointName", - Protocol: "TCP", - Port: 12345, - }, - }, - }}, - } - - err = s.k8sClient.Create(ctx, &endpointsToCreate) + err = k8sClient.Create(ctx, &endpointsToCreate) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -393,8 +392,31 @@ func (s *PodControllerTestSuite) TestDottedPodNamesDns() { func (s *PodControllerTestSuite) TestDottedServiceNamesDns() { ctx := context.TODO() + endpointsToCreate := corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{Name: "foo-svc.3.0.0", Namespace: "bar"}, + Subsets: []corev1.EndpointSubset{{ + Addresses: []corev1.EndpointAddress{ + { + IP: "123.123.123.123", + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Namespace: "bar", + Name: "foo", + }, + }, + }, + Ports: []corev1.EndpointPort{ + { + Name: "endpointName", + Protocol: "TCP", + Port: 12345, + }, + }, + }}, + } + k8sClient := createK8sClientWithEndpoint(&endpointsToCreate, "foo") r := NewPodReconciler( - s.k8sClient, + k8sClient, s.log, scheme.Scheme, podControllerTestTrustDomain, @@ -422,33 +444,10 @@ func (s *PodControllerTestSuite) TestDottedServiceNamesDns() { PodIP: "123.123.123.124", }, } - err := s.k8sClient.Create(ctx, &pod) + err := k8sClient.Create(ctx, &pod) s.Assert().NoError(err) - endpointsToCreate := corev1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{Name: "foo-svc.3.0.0", Namespace: "bar"}, - Subsets: []corev1.EndpointSubset{{ - Addresses: []corev1.EndpointAddress{ - { - IP: "123.123.123.123", - TargetRef: &corev1.ObjectReference{ - Kind: "Pod", - Namespace: "bar", - Name: "foo", - }, - }, - }, - Ports: []corev1.EndpointPort{ - { - Name: "endpointName", - Protocol: "TCP", - Port: 12345, - }, - }, - }}, - } - - err = s.k8sClient.Create(ctx, &endpointsToCreate) + err = k8sClient.Create(ctx, &endpointsToCreate) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -476,8 +475,9 @@ func (s *PodControllerTestSuite) TestDottedServiceNamesDns() { func (s *PodControllerTestSuite) TestSkipsDisabledNamespace() { ctx := context.TODO() + k8sClient := createK8sClient() r := NewPodReconciler( - s.k8sClient, + k8sClient, s.log, scheme.Scheme, podControllerTestTrustDomain, @@ -505,7 +505,7 @@ func (s *PodControllerTestSuite) TestSkipsDisabledNamespace() { PodIP: "123.123.123.124", }, } - err := s.k8sClient.Create(ctx, &pod) + err := k8sClient.Create(ctx, &pod) s.Assert().NoError(err) _, err = r.Reconcile(ctx, ctrl.Request{ @@ -522,3 +522,19 @@ func (s *PodControllerTestSuite) TestSkipsDisabledNamespace() { s.Assert().NoError(err) s.Assert().Len(es, 0) } + +func createK8sClient() client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + Build() +} + +// createK8sClientWithEndpoint add Index to client, that is used to filter resources +func createK8sClientWithEndpoint(endpoints *corev1.Endpoints, uid string) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithIndex(endpoints, + endpointSubsetAddressReferenceField, + func(client.Object) []string { return []string{uid} }). + Build() +} diff --git a/support/oidc-discovery-provider/README.md b/support/oidc-discovery-provider/README.md index 5bf0bf4463..730cfa61cf 100644 --- a/support/oidc-discovery-provider/README.md +++ b/support/oidc-discovery-provider/README.md @@ -52,6 +52,8 @@ The configuration file is **required** by the provider. It contains |--------------------------|--------|----------------|------------------------------------------------------|---------| | `listen_named_pipe_name` | string | required[1][3] | Pipe name to listen with a named pipe. Windows only. | | + + #### Considerations for Unix platforms [1]: One of `acme` or `listen_socket_path` must be defined. @@ -88,7 +90,7 @@ will terminate if another domain is requested. | Key | Type | Required? | Description | Default | |-----------------|----------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| -| `address` | string | required | SPIRE Server API gRPC target address. Only the unix name system is supported. See https://github.com/grpc/grpc/blob/master/doc/naming.md. Unix platforms only. | | +| `address` | string | required | SPIRE Server API gRPC target address. Only the unix name system is supported. See . Unix platforms only. | | | `experimental` | section | optional | The experimental options that are subject to change or removal. | | | `poll_interval` | duration | optional | How often to poll for changes to the public key material. | `"10s"` | @@ -130,7 +132,7 @@ Both states respond with a 200 OK status code for success or 500 Internal Server #### Server API -``` +```hcl log_level = "debug" domains = ["mypublicdomain.test"] acme { @@ -145,7 +147,7 @@ server_api { #### Workload API -``` +```hcl log_level = "debug" domains = ["mypublicdomain.test"] acme { @@ -165,7 +167,7 @@ The following configuration has the OIDC Discovery Provider listen for requests on the given socket. This can be used in conjunction with a webserver like Nginx, Apache, or Envoy which supports reverse proxying to a unix socket. -``` +```hcl log_level = "debug" domains = ["mypublicdomain.test"] listen_socket_path = "/run/oidc-discovery-provider/server.sock" @@ -179,7 +181,7 @@ workload_api { A minimal Nginx configuration that proxies all traffic to the OIDC Discovery Provider's socket might look like this. -``` +```nginx daemon off; events {} http { @@ -200,7 +202,7 @@ daemon off; #### Server API -``` +```hcl log_level = "debug" domains = ["mypublicdomain.test"] acme { @@ -217,7 +219,7 @@ server_api { #### Workload API -``` +```hcl log_level = "debug" domains = ["mypublicdomain.test"] acme { @@ -239,7 +241,7 @@ The following configuration has the OIDC Discovery Provider listen for requests on the given named pipe. This can be used in conjunction with a webserver that supports reverse proxying to a named pipe. -``` +```hcl log_level = "debug" domains = ["mypublicdomain.test"] experimental { diff --git a/support/oidc-discovery-provider/main.go b/support/oidc-discovery-provider/main.go index 57f391b7a8..d5cc2e13a6 100644 --- a/support/oidc-discovery-provider/main.go +++ b/support/oidc-discovery-provider/main.go @@ -100,7 +100,7 @@ func run(configPath string) error { if config.HealthChecks != nil { go func() { server := &http.Server{ - Addr: fmt.Sprintf("localhost:%d", config.HealthChecks.BindPort), + Addr: fmt.Sprintf(":%d", config.HealthChecks.BindPort), Handler: NewHealthChecksHandler(source, config), ReadHeaderTimeout: 10 * time.Second, } diff --git a/test/fakes/fakeagentkeymanager/keymanager.go b/test/fakes/fakeagentkeymanager/keymanager.go index 349ef1b885..73908dce83 100644 --- a/test/fakes/fakeagentkeymanager/keymanager.go +++ b/test/fakes/fakeagentkeymanager/keymanager.go @@ -7,15 +7,16 @@ import ( "github.com/spiffe/spire/pkg/agent/plugin/keymanager/disk" "github.com/spiffe/spire/pkg/agent/plugin/keymanager/memory" "github.com/spiffe/spire/test/plugintest" + "github.com/spiffe/spire/test/testkey" ) // New returns a fake key manager func New(t *testing.T, dir string) keymanager.KeyManager { km := new(keymanager.V1) if dir != "" { - plugintest.Load(t, disk.BuiltIn(), km, plugintest.Configuref("directory = %q", dir)) + plugintest.Load(t, disk.TestBuiltIn(&testkey.Generator{}), km, plugintest.Configuref("directory = %q", dir)) } else { - plugintest.Load(t, memory.BuiltIn(), km) + plugintest.Load(t, memory.TestBuiltIn(&testkey.Generator{}), km) } return km } diff --git a/test/fakes/fakeserverkeymanager/keymanager.go b/test/fakes/fakeserverkeymanager/keymanager.go index 53227f7a39..bd345960c6 100644 --- a/test/fakes/fakeserverkeymanager/keymanager.go +++ b/test/fakes/fakeserverkeymanager/keymanager.go @@ -12,14 +12,9 @@ import ( ) func New(t *testing.T) keymanager.KeyManager { - keys := new(testkey.Keys) - plugin := keyManager{ - Base: keymanagerbase.New(keymanagerbase.Funcs{ - GenerateRSA2048Key: keys.NextRSA2048, - GenerateRSA4096Key: keys.NextRSA4096, - GenerateEC256Key: keys.NextEC256, - GenerateEC384Key: keys.NextEC384, + Base: keymanagerbase.New(keymanagerbase.Config{ + Generator: &testkey.Generator{}, }), } diff --git a/test/fixture/config/agent_run_posix.conf b/test/fixture/config/agent_run_posix.conf new file mode 100644 index 0000000000..9c9ae55255 --- /dev/null +++ b/test/fixture/config/agent_run_posix.conf @@ -0,0 +1,9 @@ +agent { + data_dir = "./.data" + log_level = "DEBUG" + server_address = "127.0.0.1" + server_port = "8081" + trust_domain = "example.org" +} + +plugins {} diff --git a/test/fixture/config/agent_run_windows.conf b/test/fixture/config/agent_run_windows.conf new file mode 100644 index 0000000000..c75a0f599e --- /dev/null +++ b/test/fixture/config/agent_run_windows.conf @@ -0,0 +1,9 @@ +agent { + insecure_bootstrap = true + log_level = "DEBUG" + server_address = "127.0.0.1" + server_port = "8081" + trust_domain = "example.org" +} + +plugins {} diff --git a/test/fixture/config/server_run_crash_posix.conf b/test/fixture/config/server_run_crash_posix.conf new file mode 100644 index 0000000000..1394556879 --- /dev/null +++ b/test/fixture/config/server_run_crash_posix.conf @@ -0,0 +1,33 @@ +server { + bind_address = "127.0.0.1" + bind_port = "8081" + socket_path = "/tmp/spire-server-test/private/api.sock" + trust_domain = "example.org" + log_level = "DEBUG" + ca_subject { + country = ["US"] + organization = ["SPIFFE"] + common_name = "" + } +} + +plugins { + DataStore "sql" { + plugin_data { + } + } + + NodeAttestor "join_token" { + plugin_data { + } + } + + KeyManager "memory" { + plugin_data = {} + } + + UpstreamAuthority "disk" { + plugin_data { + } + } +} diff --git a/test/fixture/config/server_run_start_posix.conf b/test/fixture/config/server_run_start_posix.conf new file mode 100644 index 0000000000..46e8ab9b3d --- /dev/null +++ b/test/fixture/config/server_run_start_posix.conf @@ -0,0 +1,37 @@ +server { + bind_address = "127.0.0.1" + bind_port = "8081" + socket_path = "/tmp/spire-server-test/private/api.sock" + trust_domain = "example.org" + log_level = "DEBUG" + ca_subject { + country = ["US"] + organization = ["SPIFFE"] + common_name = "" + } +} + +plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "$SPIRE_SERVER_TEST_DATA_CONNECTION" + } + } + + NodeAttestor "join_token" { + plugin_data { + } + } + + KeyManager "memory" { + plugin_data = {} + } + + UpstreamAuthority "disk" { + plugin_data { + key_file_path = "../../../../conf/server/dummy_upstream_ca.key" + cert_file_path = "../../../../conf/server/dummy_upstream_ca.crt" + } + } +} diff --git a/test/fixture/config/server_run_windows.conf b/test/fixture/config/server_run_windows.conf new file mode 100644 index 0000000000..a3542b1a44 --- /dev/null +++ b/test/fixture/config/server_run_windows.conf @@ -0,0 +1,13 @@ +server { + bind_address = "127.0.0.1" + bind_port = "8081" + trust_domain = "example.org" + log_level = "DEBUG" + ca_subject { + country = ["US"] + organization = ["SPIFFE"] + common_name = "" + } +} + +plugins {} diff --git a/test/fixture/registration/good-for-update.json b/test/fixture/registration/good-for-update.json index 274ad892fe..b1e12cb49a 100644 --- a/test/fixture/registration/good-for-update.json +++ b/test/fixture/registration/good-for-update.json @@ -10,7 +10,8 @@ ], "spiffe_id": "spiffe://example.org/Blog", "parent_id": "spiffe://example.org/spire/agent/join_token/TokenBlog", - "ttl": 200, + "x509_svid_ttl": 200, + "jwt_svid_ttl": 300, "admin": true }, { @@ -23,7 +24,8 @@ ], "spiffe_id": "spiffe://example.org/Database", "parent_id": "spiffe://example.org/spire/agent/join_token/TokenDatabase", - "ttl": 200 + "x509_svid_ttl": 200, + "jwt_svid_ttl": 300 }, { "entry_id": "entry-id-3", @@ -40,7 +42,8 @@ "spiffe_id": "spiffe://example.org/Storesvid", "parent_id": "spiffe://example.org/spire/agent/join_token/TokenDatabase", "store_svid": true, - "ttl": 200 + "x509_svid_ttl": 200, + "jwt_svid_ttl": 300 } ] } diff --git a/test/fixture/registration/good.json b/test/fixture/registration/good.json index dad588ef89..f0c7fe564f 100644 --- a/test/fixture/registration/good.json +++ b/test/fixture/registration/good.json @@ -9,7 +9,8 @@ ], "spiffe_id": "spiffe://example.org/Blog", "parent_id": "spiffe://example.org/spire/agent/join_token/TokenBlog", - "ttl": 200, + "x509_svid_ttl": 200, + "jwt_svid_ttl": 30, "admin": true }, { @@ -21,7 +22,8 @@ ], "spiffe_id": "spiffe://example.org/Database", "parent_id": "spiffe://example.org/spire/agent/join_token/TokenDatabase", - "ttl": 200 + "x509_svid_ttl": 200, + "jwt_svid_ttl": 30 }, { "selectors": [ @@ -36,7 +38,8 @@ ], "spiffe_id": "spiffe://example.org/storesvid", "parent_id": "spiffe://example.org/spire/agent/join_token/TokenDatabase", - "ttl": 200, + "x509_svid_ttl": 200, + "jwt_svid_ttl": 30, "store_svid": true } ] diff --git a/test/integration/common b/test/integration/common index 54e7a7651c..4a007a584a 100644 --- a/test/integration/common +++ b/test/integration/common @@ -40,6 +40,30 @@ docker-up() { docker-compose up -d "$@" || fail-now "failed to bring up services." } +docker-wait-for-healthy() { + if [ $# -ne 3 ]; then + fail-now "docker-wait-for-healthy: " + fi + + local ctr_name=$1 + local maxchecks=$2 + local interval=$3 + for ((i=1;i<=maxchecks;i++)); do + set +e + health_status=$(docker inspect --format '{{.State.Health.Status}}' "${ctr_name}" 2>/dev/null) + if [ "${health_status}" == "healthy" ]; then + return + else + log-debug "waiting for container ${ctr_name} to launch" + fi + set -e + + sleep "${interval}" + done + + fail-now "timed out waiting for ${ctr_name} to start" +} + docker-stop() { if [ $# -eq 0 ]; then log-debug "stopping services..." @@ -108,7 +132,7 @@ build-mashup-image() { ENVOY_IMAGE_TAG="${ENVOY_VERSION}-latest" cat > Dockerfile < Dockerfile < conf/upstream/server/federated-domain.test.bundle + + # On macOS, there can be a delay propagating the file on the bind mount to the other container + sleep 1 + docker-compose exec -T upstream-spire-server \ /opt/spire/bin/spire-server federation create \ -bundleEndpointProfile "https_spiffe" \ @@ -32,6 +36,10 @@ setup-tests() { log-debug "bootstrapping bundle from upstream to downstream federated server..." docker-compose exec -T upstream-spire-server \ /opt/spire/bin/spire-server bundle show -format spiffe > conf/downstream-federated/server/domain.test.bundle + + # On macOS, there can be a delay propagating the file on the bind mount to the other container + sleep 1 + docker-compose exec -T downstream-federated-spire-server \ /opt/spire/bin/spire-server bundle set -format spiffe -id spiffe://domain.test -path /opt/spire/conf/server/domain.test.bundle diff --git a/test/integration/suites/envoy-sds-v3-spiffe-auth/Dockerfile b/test/integration/suites/envoy-sds-v3-spiffe-auth/Dockerfile index 1bf9c313e2..f7a74e6c29 100644 --- a/test/integration/suites/envoy-sds-v3-spiffe-auth/Dockerfile +++ b/test/integration/suites/envoy-sds-v3-spiffe-auth/Dockerfile @@ -1,4 +1,4 @@ -FROM spire-agent:latest-local as spire-agent +FROM spire-agent-scratch:latest-local as spire-agent FROM envoyproxy/envoy-alpine:v1.19.0 AS envoy-agent-mashup COPY --from=spire-agent /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent diff --git a/test/integration/suites/envoy-sds-v3-spiffe-auth/README.md b/test/integration/suites/envoy-sds-v3-spiffe-auth/README.md index 3c9f590d14..ef76b278ee 100644 --- a/test/integration/suites/envoy-sds-v3-spiffe-auth/README.md +++ b/test/integration/suites/envoy-sds-v3-spiffe-auth/README.md @@ -4,7 +4,7 @@ Exercises [Envoy](https://www.envoyproxy.io/) [SDS](https://www.envoyproxy.io/docs/envoy/latest/configuration/security/secret) -compatability within SPIRE by wiring up two workloads that achieve connectivity +compatibility within SPIRE by wiring up two workloads that achieve connectivity using Envoy backed with identities and trust information retrieved from the SPIRE agent SDS implementation. Using [SPIFFE Validator](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.proto) for certificates handshake. @@ -13,7 +13,6 @@ A customer container image is used that runs both Envoy and the SPIRE Agent. Soc The test ensures both TLS and mTLS connectivity between the workload. This is exercised with a federated workload and also with a not federated workload. - upstream-spire-server downtream-federated-spire-server / \ | / \ | @@ -21,4 +20,3 @@ The test ensures both TLS and mTLS connectivity between the workload. This is ex / \ | / \ | | | | | downtream-socat-mtls downstream-socat-tls upstream-socat downstream-federated-socat-mtls downstream-federated-socat-tls - diff --git a/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/downstream-federated/server/server.conf b/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/downstream-federated/server/server.conf index 56723f47df..a457dae5f6 100644 --- a/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/downstream-federated/server/server.conf +++ b/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/downstream-federated/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "5m" + default_x509_svid_ttl = "5m" federation { bundle_endpoint { diff --git a/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/upstream/server/server.conf b/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/upstream/server/server.conf index 4053328a15..205a5c8c61 100644 --- a/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/upstream/server/server.conf +++ b/test/integration/suites/envoy-sds-v3-spiffe-auth/conf/upstream/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "5m" + default_x509_svid_ttl = "5m" federation { bundle_endpoint { diff --git a/test/integration/suites/envoy-sds-v3-spiffe-auth/docker-compose.yaml b/test/integration/suites/envoy-sds-v3-spiffe-auth/docker-compose.yaml index 20a029ecc3..4f2c7af485 100644 --- a/test/integration/suites/envoy-sds-v3-spiffe-auth/docker-compose.yaml +++ b/test/integration/suites/envoy-sds-v3-spiffe-auth/docker-compose.yaml @@ -1,13 +1,13 @@ version: '3' services: upstream-spire-server: - image: spire-server:latest-local - hostname: upstream-spire-sever + image: spire-server-scratch:latest-local + hostname: upstream-spire-server volumes: - ./conf/upstream/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] downstream-federated-spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: downstream-federated-spire-server volumes: - ./conf/downstream-federated/server:/opt/spire/conf/server diff --git a/test/integration/suites/envoy-sds-v3/README.md b/test/integration/suites/envoy-sds-v3/README.md index ecb5c8053b..0b4e883a25 100644 --- a/test/integration/suites/envoy-sds-v3/README.md +++ b/test/integration/suites/envoy-sds-v3/README.md @@ -4,7 +4,7 @@ Exercises [Envoy](https://www.envoyproxy.io/) [SDS](https://www.envoyproxy.io/docs/envoy/latest/configuration/security/secret) -compatability within SPIRE by wiring up two workloads that achieve connectivity +compatibility within SPIRE by wiring up two workloads that achieve connectivity using Envoy backed with identities and trust information retrieved from the SPIRE agent SDS implementation. diff --git a/test/integration/suites/envoy-sds-v3/conf/server/server.conf b/test/integration/suites/envoy-sds-v3/conf/server/server.conf index 4f3734bdcb..071642c35b 100644 --- a/test/integration/suites/envoy-sds-v3/conf/server/server.conf +++ b/test/integration/suites/envoy-sds-v3/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "1m" + default_x509_svid_ttl = "1m" } plugins { diff --git a/test/integration/suites/envoy-sds-v3/docker-compose.yaml b/test/integration/suites/envoy-sds-v3/docker-compose.yaml index 3adb5163b8..c37ca50a9a 100644 --- a/test/integration/suites/envoy-sds-v3/docker-compose.yaml +++ b/test/integration/suites/envoy-sds-v3/docker-compose.yaml @@ -1,7 +1,7 @@ version: '3' services: spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: spire-server volumes: - ./conf/server:/opt/spire/conf/server diff --git a/test/integration/suites/evict-agent/conf/server/server.conf b/test/integration/suites/evict-agent/conf/server/server.conf index 308378faf5..cc267504e2 100644 --- a/test/integration/suites/evict-agent/conf/server/server.conf +++ b/test/integration/suites/evict-agent/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "20m" - default_svid_ttl = "10m" + default_x509_svid_ttl = "10m" } plugins { diff --git a/test/integration/suites/evict-agent/docker-compose.yaml b/test/integration/suites/evict-agent/docker-compose.yaml index 0e67183c23..0e5b71f908 100644 --- a/test/integration/suites/evict-agent/docker-compose.yaml +++ b/test/integration/suites/evict-agent/docker-compose.yaml @@ -1,13 +1,13 @@ version: '3' services: spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: spire-server volumes: - ./conf/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] spire-agent: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local hostname: spire-agent depends_on: ["spire-server"] volumes: diff --git a/test/integration/suites/fetch-x509-svids/conf/server/server.conf b/test/integration/suites/fetch-x509-svids/conf/server/server.conf index a8f18c0680..b6b82f9371 100644 --- a/test/integration/suites/fetch-x509-svids/conf/server/server.conf +++ b/test/integration/suites/fetch-x509-svids/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "10m" + default_x509_svid_ttl = "10m" } plugins { diff --git a/test/integration/suites/fetch-x509-svids/docker-compose.yaml b/test/integration/suites/fetch-x509-svids/docker-compose.yaml index 0e67183c23..0e5b71f908 100644 --- a/test/integration/suites/fetch-x509-svids/docker-compose.yaml +++ b/test/integration/suites/fetch-x509-svids/docker-compose.yaml @@ -1,13 +1,13 @@ version: '3' services: spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: spire-server volumes: - ./conf/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] spire-agent: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local hostname: spire-agent depends_on: ["spire-server"] volumes: diff --git a/test/integration/suites/ghostunnel-federation/02-bootstrap-federation-and-agents b/test/integration/suites/ghostunnel-federation/02-bootstrap-federation-and-agents index af78886ac4..ac5d224c63 100755 --- a/test/integration/suites/ghostunnel-federation/02-bootstrap-federation-and-agents +++ b/test/integration/suites/ghostunnel-federation/02-bootstrap-federation-and-agents @@ -13,11 +13,19 @@ docker-compose exec -T upstream-spire-server \ log-debug "bootstrapping bundle from downstream to upstream server..." docker-compose exec -T downstream-spire-server \ /opt/spire/bin/spire-server bundle show -format spiffe > conf/upstream/server/downstream-domain.test.bundle + +# On macOS, there can be a delay propagating the file on the bind mount to the other container +sleep 1 + docker-compose exec -T upstream-spire-server \ /opt/spire/bin/spire-server bundle set -format spiffe -id spiffe://downstream-domain.test -path /opt/spire/conf/server/downstream-domain.test.bundle log-debug "bootstrapping bundle from upstream to downstream server..." docker-compose exec -T upstream-spire-server \ /opt/spire/bin/spire-server bundle show -format spiffe > conf/downstream/server/upstream-domain.test.bundle + +# On macOS, there can be a delay propagating the file on the bind mount to the other container +sleep 1 + docker-compose exec -T downstream-spire-server \ /opt/spire/bin/spire-server bundle set -format spiffe -id spiffe://upstream-domain.test -path /opt/spire/conf/server/upstream-domain.test.bundle diff --git a/test/integration/suites/ghostunnel-federation/Dockerfile b/test/integration/suites/ghostunnel-federation/Dockerfile index 08d71be888..c5a6f66a56 100644 --- a/test/integration/suites/ghostunnel-federation/Dockerfile +++ b/test/integration/suites/ghostunnel-federation/Dockerfile @@ -1,11 +1,11 @@ -FROM spire-agent:latest-local as spire-agent +FROM spire-agent-scratch:latest-local as spire-agent FROM ghostunnel/ghostunnel:latest AS ghostunnel-latest FROM alpine/socat:latest AS socat-ghostunnel-agent-mashup -COPY --from=spire-agent /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent -COPY --from=ghostunnel-latest /usr/bin/ghostunnel /usr/bin/ghostunnel -RUN apk --no-cache add dumb-init -RUN apk --no-cache add supervisor ENTRYPOINT ["/usr/bin/dumb-init", "supervisord", "--nodaemon", "--configuration", "/opt/supervisord/supervisord.conf"] CMD [] +COPY --from=spire-agent /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent +COPY --from=ghostunnel-latest /usr/bin/ghostunnel /usr/bin/ghostunnel +RUN apk --no-cache --update add dumb-init +RUN apk --no-cache --update add supervisor diff --git a/test/integration/suites/ghostunnel-federation/conf/downstream/server/server.conf b/test/integration/suites/ghostunnel-federation/conf/downstream/server/server.conf index fe74c3e751..f9876be8cb 100644 --- a/test/integration/suites/ghostunnel-federation/conf/downstream/server/server.conf +++ b/test/integration/suites/ghostunnel-federation/conf/downstream/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "5m" + default_x509_svid_ttl = "5m" federation { bundle_endpoint { diff --git a/test/integration/suites/ghostunnel-federation/conf/upstream/server/server.conf b/test/integration/suites/ghostunnel-federation/conf/upstream/server/server.conf index 7ab66f8ab8..27483708c1 100644 --- a/test/integration/suites/ghostunnel-federation/conf/upstream/server/server.conf +++ b/test/integration/suites/ghostunnel-federation/conf/upstream/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "5m" + default_x509_svid_ttl = "5m" federation { bundle_endpoint { diff --git a/test/integration/suites/ghostunnel-federation/docker-compose.yaml b/test/integration/suites/ghostunnel-federation/docker-compose.yaml index bdb531edae..fc15bb17ea 100644 --- a/test/integration/suites/ghostunnel-federation/docker-compose.yaml +++ b/test/integration/suites/ghostunnel-federation/docker-compose.yaml @@ -1,12 +1,12 @@ version: '3' services: upstream-spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local volumes: - ./conf/upstream/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] downstream-spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local volumes: - ./conf/downstream/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] diff --git a/test/integration/suites/join-token/docker-compose.yaml b/test/integration/suites/join-token/docker-compose.yaml index 6e2fc0c222..079cc5156c 100644 --- a/test/integration/suites/join-token/docker-compose.yaml +++ b/test/integration/suites/join-token/docker-compose.yaml @@ -1,17 +1,17 @@ version: '3' services: spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local volumes: - ./conf/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] spire-agent: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local volumes: - ./conf/agent:/opt/spire/conf/agent command: ["-config", "/opt/spire/conf/agent/agent.conf"] bad-spire-agent: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local volumes: - ./conf/bad-agent:/opt/spire/conf/agent command: ["-config", "/opt/spire/conf/agent/agent.conf"] diff --git a/test/integration/suites/k8s-crd-mode/00-setup b/test/integration/suites/k8s-crd-mode/00-setup index fd0e7ed42b..f96e54ba9f 100755 --- a/test/integration/suites/k8s-crd-mode/00-setup +++ b/test/integration/suites/k8s-crd-mode/00-setup @@ -25,7 +25,7 @@ rm conf/kind-config.yaml.bak start-kind-cluster "${KIND_PATH}" k8stest ./conf/kind-config.yaml # Load the given images in the cluster. -container_images=("spire-server:latest-local" "spire-agent:latest-local" "k8s-workload-registrar:latest-local" "example-crd-agent:latest") +container_images=("spire-server-scratch:latest-local" "spire-agent-scratch:latest-local" "k8s-workload-registrar:latest-local" "example-crd-agent:latest") load-images "${KIND_PATH}" k8stest "${container_images[@]}" # Set the kubectl context. diff --git a/test/integration/suites/k8s-crd-mode/Dockerfile b/test/integration/suites/k8s-crd-mode/Dockerfile index bc76a647c4..5c129549d9 100644 --- a/test/integration/suites/k8s-crd-mode/Dockerfile +++ b/test/integration/suites/k8s-crd-mode/Dockerfile @@ -1,4 +1,6 @@ -FROM spire-agent:latest-local AS example-crd-agent -RUN apk add --update openssl && \ - rm -rf /var/cache/apk/* +FROM alpine:3.17 AS example-crd-agent CMD [] +RUN apk add --no-cache --update dumb-init +RUN apk add --no-cache --update openssl + +COPY --from=spire-agent-scratch:latest-local /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent diff --git a/test/integration/suites/k8s-crd-mode/conf/agent/spire-agent.yaml b/test/integration/suites/k8s-crd-mode/conf/agent/spire-agent.yaml index ca5fa1f1c7..fc900c094c 100644 --- a/test/integration/suites/k8s-crd-mode/conf/agent/spire-agent.yaml +++ b/test/integration/suites/k8s-crd-mode/conf/agent/spire-agent.yaml @@ -107,16 +107,9 @@ spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet serviceAccountName: spire-agent - initContainers: - - name: init - # This is a small image with wait-for-it, choose whatever image - # you prefer that waits for a service to be up. This image is built - # from https://github.com/lqhl/wait-for-it - image: gcr.io/spiffe-io/wait-for-it - args: ["-t", "30", "spire-server:8081"] containers: - name: spire-agent - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local imagePullPolicy: Never args: ["-config", "/run/spire/config/agent.conf"] volumeMounts: diff --git a/test/integration/suites/k8s-crd-mode/conf/server/spire-server.yaml b/test/integration/suites/k8s-crd-mode/conf/server/spire-server.yaml index bc2c3c11f3..bdbaec144d 100644 --- a/test/integration/suites/k8s-crd-mode/conf/server/spire-server.yaml +++ b/test/integration/suites/k8s-crd-mode/conf/server/spire-server.yaml @@ -64,7 +64,7 @@ data: trust_domain = "example.org" data_dir = "/run/spire/data" log_level = "DEBUG" - default_svid_ttl = "1h" + default_x509_svid_ttl = "1h" ca_subject = { country = ["US"], organization = ["SPIFFE"], @@ -211,7 +211,7 @@ spec: shareProcessNamespace: true containers: - name: spire-server - image: spire-server:latest-local + image: spire-server-scratch:latest-local imagePullPolicy: Never args: ["-config", "/run/spire/config/server.conf"] ports: diff --git a/test/integration/suites/k8s-reconcile/conf/agent/spire-agent.yaml b/test/integration/suites/k8s-reconcile/conf/agent/spire-agent.yaml index 7d77963bf7..86a830ffbb 100644 --- a/test/integration/suites/k8s-reconcile/conf/agent/spire-agent.yaml +++ b/test/integration/suites/k8s-reconcile/conf/agent/spire-agent.yaml @@ -111,13 +111,6 @@ spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet serviceAccountName: spire-agent - initContainers: - - name: init - # This is a small image with wait-for-it, choose whatever image - # you prefer that waits for a service to be up. This image is built - # from https://github.com/lqhl/wait-for-it - image: gcr.io/spiffe-io/wait-for-it - args: ["-t", "30", "spire-server:8081"] containers: - name: spire-agent image: spire-agent-scratch:latest-local diff --git a/test/integration/suites/k8s-reconcile/conf/server/spire-server.yaml b/test/integration/suites/k8s-reconcile/conf/server/spire-server.yaml index 5184a43ecc..7b119c016c 100644 --- a/test/integration/suites/k8s-reconcile/conf/server/spire-server.yaml +++ b/test/integration/suites/k8s-reconcile/conf/server/spire-server.yaml @@ -114,7 +114,7 @@ data: trust_domain = "example.org" data_dir = "/run/spire/data" log_level = "DEBUG" - default_svid_ttl = "1h" + default_x509_svid_ttl = "1h" ca_ttl = "12h" ca_subject { country = ["US"] diff --git a/test/integration/suites/nested-rotation/00-setup b/test/integration/suites/nested-rotation/00-setup index cb4f4eac10..8d8fb9f846 100755 --- a/test/integration/suites/nested-rotation/00-setup +++ b/test/integration/suites/nested-rotation/00-setup @@ -35,4 +35,4 @@ sed -i.bak "s#CGROUP_MATCHERS#$CGROUP_MATCHERS#" root/agent/agent.conf sed -i.bak "s#CGROUP_MATCHERS#$CGROUP_MATCHERS#" intermediateA/agent/agent.conf sed -i.bak "s#CGROUP_MATCHERS#$CGROUP_MATCHERS#" intermediateB/agent/agent.conf -docker build --target nested-agent -t nested-agent . +docker build --target nested-agent-alpine -t nested-agent-alpine . diff --git a/test/integration/suites/nested-rotation/Dockerfile b/test/integration/suites/nested-rotation/Dockerfile index 9f8efdca87..bf2c0aeaeb 100644 --- a/test/integration/suites/nested-rotation/Dockerfile +++ b/test/integration/suites/nested-rotation/Dockerfile @@ -1,4 +1,4 @@ -FROM spire-agent:latest-local AS nested-agent -RUN apk add --update openssl && \ - rm -rf /var/cache/apk/* -CMD [] +FROM alpine:3.17 as nested-agent-alpine +RUN apk add --no-cache --update openssl +COPY --from=spire-agent-scratch:latest-local /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent +ENTRYPOINT ["/opt/spire/bin/spire-agent", "run"] diff --git a/test/integration/suites/nested-rotation/docker-compose.yaml b/test/integration/suites/nested-rotation/docker-compose.yaml index 16a75d201c..b89191c51f 100644 --- a/test/integration/suites/nested-rotation/docker-compose.yaml +++ b/test/integration/suites/nested-rotation/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3' services: # Root root-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: root-server volumes: - ./root/server:/opt/spire/conf/server @@ -10,7 +10,7 @@ services: root-agent: # Share the host pid namespace so this agent can attest the intermediate servers pid: "host" - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local depends_on: ["root-server"] hostname: root-agent volumes: @@ -23,7 +23,7 @@ services: intermediateA-server: # Share the host pid namespace so this server can be attested by the root agent pid: "host" - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: intermediateA-server labels: # label to attest server against root-agent @@ -37,7 +37,7 @@ services: intermediateA-agent: # Share the host pid namespace so this agent can attest the leafA server pid: "host" - image: spire-agent:latest-local + image: nested-agent-alpine hostname: intermediateA-agent depends_on: ["intermediateA-server"] volumes: @@ -50,7 +50,7 @@ services: leafA-server: # Share the host pid namespace so this server can be attested by the intermediateA agent pid: "host" - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: leafA-server labels: # Label to attest server against intermediateA-agent @@ -62,7 +62,7 @@ services: - ./leafA/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] leafA-agent: - image: spire-agent:latest-local + image: nested-agent-alpine hostname: leafA-agent depends_on: ["intermediateA-server"] volumes: @@ -72,7 +72,7 @@ services: intermediateB-server: # Share the host pid namespace so this server can be attested by the root agent pid: "host" - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: intermediateB-server depends_on: ["root-server","root-agent"] labels: @@ -86,7 +86,7 @@ services: intermediateB-agent: # Share the host pid namespace so this agent can attest the leafB server pid: "host" - image: nested-agent + image: nested-agent-alpine hostname: intermediateB-agent depends_on: ["intermediateB-server"] volumes: @@ -99,7 +99,7 @@ services: leafB-server: # Share the host pid namespace so this server can be attested by the intermediateB agent pid: "host" - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: leafB-server depends_on: ["intermediateB-server","intermediateB-agent"] labels: @@ -111,7 +111,7 @@ services: - ./leafB/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] leafB-agent: - image: nested-agent + image: nested-agent-alpine hostname: leafB-agent depends_on: ["leafB-server"] volumes: diff --git a/test/integration/suites/nested-rotation/intermediateA/server/server.conf b/test/integration/suites/nested-rotation/intermediateA/server/server.conf index a8941d9a5d..f3524694a1 100644 --- a/test/integration/suites/nested-rotation/intermediateA/server/server.conf +++ b/test/integration/suites/nested-rotation/intermediateA/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "15s" + default_svid_ttl = "15s" # TODO: Update to use default_x509_svid_ttl in 1.6.0. Keep in previous releases for testing. } plugins { diff --git a/test/integration/suites/nested-rotation/intermediateB/server/server.conf b/test/integration/suites/nested-rotation/intermediateB/server/server.conf index a8941d9a5d..f3524694a1 100644 --- a/test/integration/suites/nested-rotation/intermediateB/server/server.conf +++ b/test/integration/suites/nested-rotation/intermediateB/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "15s" + default_svid_ttl = "15s" # TODO: Update to use default_x509_svid_ttl in 1.6.0. Keep in previous releases for testing. } plugins { diff --git a/test/integration/suites/nested-rotation/leafA/server/server.conf b/test/integration/suites/nested-rotation/leafA/server/server.conf index e00d61dfc3..cabaa83057 100644 --- a/test/integration/suites/nested-rotation/leafA/server/server.conf +++ b/test/integration/suites/nested-rotation/leafA/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "90s" - default_svid_ttl = "15s" + default_svid_ttl = "15s" # TODO: Update to use default_x509_svid_ttl in 1.6.0. Keep in previous releases for testing. } plugins { diff --git a/test/integration/suites/nested-rotation/leafB/server/server.conf b/test/integration/suites/nested-rotation/leafB/server/server.conf index 8aa8b941bc..9d3990838c 100644 --- a/test/integration/suites/nested-rotation/leafB/server/server.conf +++ b/test/integration/suites/nested-rotation/leafB/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "90s" - default_svid_ttl = "15s" + default_svid_ttl = "15s" # TODO: Update to use default_x509_svid_ttl in 1.6.0. Keep in previous releases for testing. } plugins { diff --git a/test/integration/suites/nested-rotation/root/server/server.conf b/test/integration/suites/nested-rotation/root/server/server.conf index 44200a7114..8da834fc8a 100644 --- a/test/integration/suites/nested-rotation/root/server/server.conf +++ b/test/integration/suites/nested-rotation/root/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "15s" + default_svid_ttl = "15s" # TODO: Update to use default_x509_svid_ttl in 1.6.0. Keep in previous releases for testing. } plugins { diff --git a/test/integration/suites/node-attestation/README.md b/test/integration/suites/node-attestation/README.md index 58e7e8d6eb..d56982ee88 100644 --- a/test/integration/suites/node-attestation/README.md +++ b/test/integration/suites/node-attestation/README.md @@ -4,4 +4,3 @@ Basic tests of the node attestation APIs using a simple fake agent The agent runs in a separate Docker container, but nothing from the real SPIRE agent is used - diff --git a/test/integration/suites/node-attestation/conf/server/server.conf b/test/integration/suites/node-attestation/conf/server/server.conf index a8f18c0680..b6b82f9371 100644 --- a/test/integration/suites/node-attestation/conf/server/server.conf +++ b/test/integration/suites/node-attestation/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "10m" + default_x509_svid_ttl = "10m" } plugins { diff --git a/test/integration/suites/node-attestation/docker-compose.yaml b/test/integration/suites/node-attestation/docker-compose.yaml index 0e67183c23..0e5b71f908 100644 --- a/test/integration/suites/node-attestation/docker-compose.yaml +++ b/test/integration/suites/node-attestation/docker-compose.yaml @@ -1,13 +1,13 @@ version: '3' services: spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: spire-server volumes: - ./conf/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] spire-agent: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local hostname: spire-agent depends_on: ["spire-server"] volumes: diff --git a/test/integration/suites/rotation/conf/server/server.conf b/test/integration/suites/rotation/conf/server/server.conf index 9c004f5ce9..58df05d4f0 100644 --- a/test/integration/suites/rotation/conf/server/server.conf +++ b/test/integration/suites/rotation/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1m" - default_svid_ttl = "10s" + default_x509_svid_ttl = "10s" } plugins { diff --git a/test/integration/suites/rotation/docker-compose.yaml b/test/integration/suites/rotation/docker-compose.yaml index 0e67183c23..0e5b71f908 100644 --- a/test/integration/suites/rotation/docker-compose.yaml +++ b/test/integration/suites/rotation/docker-compose.yaml @@ -1,13 +1,13 @@ version: '3' services: spire-server: - image: spire-server:latest-local + image: spire-server-scratch:latest-local hostname: spire-server volumes: - ./conf/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] spire-agent: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local hostname: spire-agent depends_on: ["spire-server"] volumes: diff --git a/test/integration/suites/spire-server-cli/01-start-server b/test/integration/suites/spire-server-cli/01-start-server index 98d32c1a3d..2de2454a1c 100755 --- a/test/integration/suites/spire-server-cli/01-start-server +++ b/test/integration/suites/spire-server-cli/01-start-server @@ -1,4 +1,5 @@ #!/bin/bash +docker build --target spire-server-alpine -t spire-server-alpine . docker-up spire-server diff --git a/test/integration/suites/spire-server-cli/02-bundle b/test/integration/suites/spire-server-cli/02-bundle index 79e2b5ac0d..3c879b9ab4 100755 --- a/test/integration/suites/spire-server-cli/02-bundle +++ b/test/integration/suites/spire-server-cli/02-bundle @@ -1,9 +1,5 @@ #!/bin/bash -# Install openssl -docker-compose exec -T spire-server \ - apk add --update openssl && rm -rf /var/cache/apk/* - # Verify 'bundle count' correctly indicates a single bundle (the server bundle) docker-compose exec -T spire-server /opt/spire/bin/spire-server bundle count | grep 1 || fail-now "failed to count 1 bundle" diff --git a/test/integration/suites/spire-server-cli/Dockerfile b/test/integration/suites/spire-server-cli/Dockerfile new file mode 100644 index 0000000000..90e99ff13c --- /dev/null +++ b/test/integration/suites/spire-server-cli/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.17 AS spire-server-alpine +RUN apk add --no-cache --update openssl +COPY --from=spire-server-scratch:latest-local /opt/spire/bin/spire-server /opt/spire/bin/spire-server +ENTRYPOINT ["/opt/spire/bin/spire-server", "run"] diff --git a/test/integration/suites/spire-server-cli/conf/server/server.conf b/test/integration/suites/spire-server-cli/conf/server/server.conf index d3539576fa..95ca171f42 100644 --- a/test/integration/suites/spire-server-cli/conf/server/server.conf +++ b/test/integration/suites/spire-server-cli/conf/server/server.conf @@ -5,7 +5,7 @@ server { data_dir = "/opt/spire/data/server" log_level = "DEBUG" ca_ttl = "1h" - default_svid_ttl = "10m" + default_x509_svid_ttl = "10m" } plugins { diff --git a/test/integration/suites/spire-server-cli/docker-compose.yaml b/test/integration/suites/spire-server-cli/docker-compose.yaml index 701f8db2af..608e03432a 100644 --- a/test/integration/suites/spire-server-cli/docker-compose.yaml +++ b/test/integration/suites/spire-server-cli/docker-compose.yaml @@ -1,24 +1,24 @@ version: '3' services: spire-server: - image: spire-server:latest-local + image: spire-server-alpine hostname: spire-server volumes: - ./conf/server:/opt/spire/conf/server - ./conf/fixture:/opt/spire/conf/fixture command: ["-config", "/opt/spire/conf/server/server.conf"] spire-agent-1: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local volumes: - ./conf/agent-1:/opt/spire/conf/agent command: ["-config", "/opt/spire/conf/agent/agent.conf"] spire-agent-2: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local volumes: - ./conf/agent-2:/opt/spire/conf/agent command: ["-config", "/opt/spire/conf/agent/agent.conf"] spire-agent-3: - image: spire-agent:latest-local + image: spire-agent-scratch:latest-local volumes: - ./conf/agent-3:/opt/spire/conf/agent command: ["-config", "/opt/spire/conf/agent/agent.conf"] diff --git a/test/integration/suites/upgrade/00-setup b/test/integration/suites/upgrade/00-setup index a96c14e64d..f2fefdd432 100755 --- a/test/integration/suites/upgrade/00-setup +++ b/test/integration/suites/upgrade/00-setup @@ -13,9 +13,16 @@ make-service() { local _version=$2 cat <> docker-compose.yaml spire-server-${_version}: + container_name: spire-server-${_version} image: ${_registry}spire-server:${_version} hostname: spire-server user: "${UID}" + healthcheck: + # TODO: Use default socket path in 1.7.0 + test: ["CMD", "/opt/spire/bin/spire-server", "healthcheck", "-socketPath", "/opt/spire/data/server/socket/api.sock"] + interval: 1s + timeout: 3s + retries: 15 networks: our-network: aliases: @@ -25,9 +32,16 @@ cat <> docker-compose.yaml - ./conf/server:/opt/spire/conf/server command: ["-config", "/opt/spire/conf/server/server.conf"] spire-agent-${_version}: + container_name: spire-agent-${_version} image: ${_registry}spire-agent:${_version} hostname: spire-agent user: "${UID}" + healthcheck: + # TODO: Use default socket path in 1.7.0 + test: ["CMD", "/opt/spire/bin/spire-agent", "healthcheck", "-socketPath", "/opt/spire/data/agent/socket/api.sock"] + interval: 1s + timeout: 3s + retries: 15 networks: - our-network volumes: @@ -51,5 +65,5 @@ EOF make-service "" latest-local while read -r version; do - make-service gcr.io/spiffe-io/ "${version}" + make-service ghcr.io/spiffe/ "${version}" done < versions.txt diff --git a/test/integration/suites/upgrade/01-run-upgrade-tests b/test/integration/suites/upgrade/01-run-upgrade-tests index bd12fdabaf..449d514b3c 100755 --- a/test/integration/suites/upgrade/01-run-upgrade-tests +++ b/test/integration/suites/upgrade/01-run-upgrade-tests @@ -8,26 +8,36 @@ # something else to create the directory first). start-old-server() { - log-info "bringing up $1 agent..." - docker-up "spire-server-$1" + local _maxchecks=15 + local _interval=1 + log-info "bringing up $1 server..." + local ctr_name="spire-server-$1" + docker-up "${ctr_name}" + docker-wait-for-healthy "${ctr_name}" "${_maxchecks}" "${_interval}" } bootstrap-agent() { - log-debug "bootstrapping $1 agent..." + # TODO: Remove -socketPath argument in 1.7.0 and rely on the default socket path docker-compose exec -T "spire-server-$1" \ /opt/spire/bin/spire-server bundle show \ - > conf/agent/bootstrap.crt + -socketPath /opt/spire/data/server/socket/api.sock > conf/agent/bootstrap.crt } start-old-agent() { + local _maxchecks=15 + local _interval=1 log-info "bringing up $1 agent..." - docker-up "spire-agent-$1" + local ctr_name="spire-agent-$1" + docker-up "${ctr_name}" + docker-wait-for-healthy "${ctr_name}" "${_maxchecks}" "${_interval}" } create-registration-entry() { log-debug "creating registration entry..." + # TODO: Remove -socketPath argument in 1.7.0 and rely on the default socket path docker-compose exec -T "spire-server-$1" \ /opt/spire/bin/spire-server entry create \ + -socketPath /opt/spire/data/server/socket/api.sock \ -parentID "spiffe://domain.test/spire/agent/x509pop/$(fingerprint conf/agent/agent.crt.pem)" \ -spiffeID "spiffe://domain.test/workload" \ -selector "unix:uid:${UID}" \ @@ -52,14 +62,18 @@ check-old-agent-svid() { log-info "checking X509-SVID on $1 agent..." docker-compose exec -T "spire-agent-$1" \ /opt/spire/bin/spire-agent api fetch x509 \ - -socketPath /tmp/agent.sock \ + -socketPath /opt/spire/data/agent/socket/api.sock \ -write /opt/test/before-server-upgrade || fail-now "SVID check failed" } upgrade-server() { + local _maxchecks=15 + local _interval=1 log-info "upgrading $1 server to latest..." docker-stop "spire-server-$1" - docker-up spire-server-latest-local + local new_ctr_name="spire-server-latest-local" + docker-up "${new_ctr_name}" + docker-wait-for-healthy "${new_ctr_name}" "${_maxchecks}" "${_interval}" check-codebase-version-is-ahead "$1" } @@ -84,9 +98,10 @@ check-old-agent-svid-after-upgrade() { for ((i=1;i<=_maxchecks;i++)); do log-info "checking X509-SVID after server upgrade ($i of $_maxchecks max)..." + # TODO: Remove -socketPath argument in 1.7.0 and rely on the default socket path docker-compose exec -T "spire-agent-$1" \ /opt/spire/bin/spire-agent api fetch x509 \ - -socketPath /tmp/agent.sock \ + -socketPath /opt/spire/data/agent/socket/api.sock \ -write /opt/test/after-server-upgrade || fail-now "SVID check failed" if ! cmp --silent svids/before-server-upgrade/svid.0.pem svids/after-server-upgrade/svid.0.pem; then # SVID has rotated @@ -98,9 +113,13 @@ check-old-agent-svid-after-upgrade() { } upgrade-agent() { + local _maxchecks=15 + local _interval=1 log-info "upgrading $1 agent to latest..." docker-stop "spire-agent-$1" - docker-up spire-agent-latest-local + local new_ctr_name="spire-agent-latest-local" + docker-up "${new_ctr_name}" + docker-wait-for-healthy "${new_ctr_name}" "${_maxchecks}" "${_interval}" } stop-and-evict-agent() { @@ -108,8 +127,10 @@ stop-and-evict-agent() { docker-stop "spire-agent-$1" log-info "evicting agent..." + # TODO: Remove -socketPath argument in 1.7.0 and rely on the default socket path docker-compose exec -T "spire-server-$1" \ /opt/spire/bin/spire-server agent evict \ + -socketPath /opt/spire/data/server/socket/api.sock \ -spiffeID "spiffe://domain.test/spire/agent/x509pop/$(fingerprint conf/agent/agent.crt.pem)" rm -rf shared/agent-data/* @@ -117,9 +138,10 @@ stop-and-evict-agent() { check-new-agent-svid-after-upgrade() { log-info "checking X509-SVID after agent upgrade..." + # TODO: Remove -socketPath argument in 1.7.0 and rely on the default socket path docker-compose exec -T spire-agent-latest-local \ /opt/spire/bin/spire-agent api fetch x509 \ - -socketPath /tmp/agent.sock \ + -socketPath /opt/spire/data/agent/socket/api.sock \ -write /opt/test/after-agent-upgrade || fail-now "SVID check failed" # SVIDs are cached in agent memory only. As the agent was restarted, there diff --git a/test/integration/suites/upgrade/README.md b/test/integration/suites/upgrade/README.md index 9d581bd03e..567576b0c9 100644 --- a/test/integration/suites/upgrade/README.md +++ b/test/integration/suites/upgrade/README.md @@ -39,5 +39,5 @@ should be removed as part of the 0.10.0 release. ## Future considerations -- Provide additional "+/- 1" SPIRE compatability checks, as currently we only +- Provide additional "+/- 1" SPIRE compatibility checks, as currently we only test that the SPIRE components start up and that SVIDs rotate. diff --git a/test/integration/suites/upgrade/conf/agent/agent.conf b/test/integration/suites/upgrade/conf/agent/agent.conf index 65d9b06f0c..a30c89f0ad 100644 --- a/test/integration/suites/upgrade/conf/agent/agent.conf +++ b/test/integration/suites/upgrade/conf/agent/agent.conf @@ -3,7 +3,7 @@ agent { log_level = "DEBUG" server_address = "spire-server" server_port = "8081" - socket_path ="/tmp/agent.sock" + socket_path ="/opt/spire/data/agent/socket/api.sock" # TODO: Use default socket path in 1.7.0 trust_bundle_path = "/opt/spire/conf/agent/bootstrap.crt" trust_domain = "domain.test" } diff --git a/test/integration/suites/upgrade/conf/server/server.conf b/test/integration/suites/upgrade/conf/server/server.conf index a339cd61a0..b355511770 100644 --- a/test/integration/suites/upgrade/conf/server/server.conf +++ b/test/integration/suites/upgrade/conf/server/server.conf @@ -4,8 +4,9 @@ server { trust_domain = "domain.test" data_dir = "/opt/spire/data/server" log_level = "DEBUG" - ca_ttl = "1m" - default_svid_ttl = "10s" + ca_ttl = "1m" + default_svid_ttl = "10s" # TODO: Update to use default_x509_svid_ttl in 1.6.0. + socket_path = "/opt/spire/data/server/socket/api.sock" # TODO: Remove this in 1.7.0 and rely on the default socket path } plugins { @@ -17,12 +18,12 @@ plugins { } NodeAttestor "x509pop" { plugin_data { - ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem" - } + ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem" + } } KeyManager "disk" { plugin_data = { - keys_path = "/opt/spire/data/server/keys.json" - } + keys_path = "/opt/spire/data/server/keys.json" + } } } diff --git a/test/integration/suites/upgrade/versions.txt b/test/integration/suites/upgrade/versions.txt index 1638f048b7..a07bf05538 100644 --- a/test/integration/suites/upgrade/versions.txt +++ b/test/integration/suites/upgrade/versions.txt @@ -3,3 +3,8 @@ 1.4.2 1.4.3 1.4.4 +1.4.5 +1.5.0 +1.5.1 +1.5.2 +1.5.3 diff --git a/test/integration/suites/upstream-authority-cert-manager/00-setup-kind b/test/integration/suites/upstream-authority-cert-manager/00-setup-kind index dd9b06d897..d208323e0a 100755 --- a/test/integration/suites/upstream-authority-cert-manager/00-setup-kind +++ b/test/integration/suites/upstream-authority-cert-manager/00-setup-kind @@ -17,7 +17,7 @@ download-kubectl "${KUBECTL_PATH}" start-kind-cluster "${KIND_PATH}" cert-manager-test ./conf/kind-config.yaml # Load the given images in the cluster. -container_images=("spire-server:latest-local") +container_images=("spire-server-scratch:latest-local") load-images "${KIND_PATH}" cert-manager-test "${container_images[@]}" # Set the kubectl context. diff --git a/test/integration/suites/upstream-authority-cert-manager/03-verify-ca b/test/integration/suites/upstream-authority-cert-manager/03-verify-ca index 724aa04512..eae1ebca2c 100755 --- a/test/integration/suites/upstream-authority-cert-manager/03-verify-ca +++ b/test/integration/suites/upstream-authority-cert-manager/03-verify-ca @@ -2,16 +2,37 @@ source init-kubectl -expLeafIssuer="issuer=C = US, O = SPIFFE" +expLeafIssuerOpenSSL="issuer=C = US, O = SPIFFE" +expCASubjectOpenSSL="subject=O = cert-manager.io, CN = example.org" + +# On macOS, /usr/bin/openssl is LibreSSL, which outputs certificate details with a different format than OpenSSL +expLeafIssuerLibreSSL="issuer= /C=US/O=SPIFFE" +expCASubjectLibreSSL="subject= /O=cert-manager.io/CN=example.org" + expLeafURI="URI:spiffe://example.org/ns/foo/sa/bar" -expCASubject="subject=O = cert-manager.io, CN = example.org" log-debug "verifying CA..." -./bin/kubectl exec -n spire $(./bin/kubectl get pod -n spire -o name) -- /opt/spire/bin/spire-server x509 mint -spiffeID spiffe://example.org/ns/foo/sa/bar -write . -leafURIResult=$(./bin/kubectl exec -n spire $(./bin/kubectl get pod -n spire -o name) -- cat svid.pem | openssl x509 -noout -text | grep URI | sed 's/^ *//g') -leafIssuerResult=$(./bin/kubectl exec -n spire $(./bin/kubectl get pod -n spire -o name) -- cat svid.pem | openssl x509 -noout -issuer) -caSubjectResult=$(./bin/kubectl exec -n spire $(./bin/kubectl get pod -n spire -o name) -- cat bundle.pem | openssl x509 -noout -subject) +mintx509svid_out=mintx509svid-out.txt +./bin/kubectl exec -n spire $(./bin/kubectl get pod -n spire -o name) -- /opt/spire/bin/spire-server x509 mint -spiffeID spiffe://example.org/ns/foo/sa/bar > $mintx509svid_out + +svid=svid.pem +sed -n '/-----BEGIN CERTIFICATE-----/,/^$/{/^$/q; p;}' $mintx509svid_out > $svid + +bundle=bundle.pem +sed -n '/Root CAs:/,/^$/p' $mintx509svid_out | sed -n '/-----BEGIN CERTIFICATE-----/,/^$/{/^$/q; p;}' > $bundle + +leafURIResult=$(openssl x509 -noout -text -in $svid | grep URI | sed 's/^ *//g') +leafIssuerResult=$(openssl x509 -noout -issuer -in $svid) +caSubjectResult=$(openssl x509 -noout -subject -in $bundle) + +if [ $(openssl version | awk '{print $1}') == 'LibreSSL' ]; then + expLeafIssuer=$expLeafIssuerLibreSSL + expCASubject=$expCASubjectLibreSSL +else + expLeafIssuer=$expLeafIssuerOpenSSL + expCASubject=$expCASubjectOpenSSL +fi if [ "$leafURIResult" != "$expLeafURI" ]; then fail-now "unexpected SPIFFE ID in resulting certificate, exp=$expLeafURI got=$leafURIResult" diff --git a/test/integration/suites/upstream-authority-cert-manager/README.md b/test/integration/suites/upstream-authority-cert-manager/README.md index cea9550fa5..efc3b55257 100644 --- a/test/integration/suites/upstream-authority-cert-manager/README.md +++ b/test/integration/suites/upstream-authority-cert-manager/README.md @@ -1,6 +1,5 @@ # Upstream Authority cert-manager Suite - ## Description This suite sets up a Kubernetes cluster using [Kind](https://kind.sigs.k8s.io), diff --git a/test/integration/suites/upstream-authority-cert-manager/conf/server/spire-server.yaml b/test/integration/suites/upstream-authority-cert-manager/conf/server/spire-server.yaml index 607f071d4a..4311386abf 100644 --- a/test/integration/suites/upstream-authority-cert-manager/conf/server/spire-server.yaml +++ b/test/integration/suites/upstream-authority-cert-manager/conf/server/spire-server.yaml @@ -53,7 +53,7 @@ data: trust_domain = "example.org" data_dir = "/run/spire/data" log_level = "DEBUG" - default_svid_ttl = "1h" + default_x509_svid_ttl = "1h" ca_ttl = "12h" ca_subject { country = ["US"] @@ -119,7 +119,7 @@ spec: shareProcessNamespace: true containers: - name: spire-server - image: spire-server:latest-local + image: spire-server-scratch:latest-local imagePullPolicy: Never args: ["-config", "/run/spire/config/server.conf"] ports: diff --git a/test/spiretest/apiserver.go b/test/spiretest/apiserver.go index 1b4425e6d4..caef4e8c5d 100644 --- a/test/spiretest/apiserver.go +++ b/test/spiretest/apiserver.go @@ -43,7 +43,7 @@ func newAPIServer(t *testing.T, registerFn func(s *grpc.Server), server *grpc.Se done := func() { assert.NoError(t, conn.Close()) - server.Stop() + server.GracefulStop() err := <-errCh switch { case err == nil, errors.Is(err, grpc.ErrServerStopped): diff --git a/test/testkey/bucket.go b/test/testkey/bucket.go new file mode 100644 index 0000000000..69711da4b5 --- /dev/null +++ b/test/testkey/bucket.go @@ -0,0 +1,116 @@ +package testkey + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "sync" + + "github.com/spiffe/spire/pkg/common/pemutil" +) + +var ( + packageDir string +) + +func init() { + packageDir = initPackageDir() +} + +func initPackageDir() string { + _, file, _, ok := runtime.Caller(0) + if !ok { + panic("unable to obtain caller information") + } + return filepath.Dir(file) +} + +type keyType[K crypto.Signer] interface { + Path() string + GenerateKey() (K, error) +} + +type bucket[KT keyType[K], K crypto.Signer] struct { + kt KT + + mtx sync.Mutex + keys []K +} + +func (b *bucket[KT, K]) At(n int) (key K, err error) { + b.mtx.Lock() + defer b.mtx.Unlock() + + if err := b.load(); err != nil { + return key, err + } + + switch { + case n > len(b.keys): + return key, errors.New("cannot ask for key beyond the end") + case n < len(b.keys): + return b.keys[n], nil + default: + key, err = b.kt.GenerateKey() + if err != nil { + return key, err + } + b.keys = append(b.keys, key) + if err := b.save(); err != nil { + return key, err + } + return key, nil + } +} + +func (b *bucket[KT, K]) load() (err error) { + if b.keys != nil { + return nil + } + + blocks, err := pemutil.LoadBlocks(b.path()) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err + } + + keys := make([]K, 0, len(blocks)) + for _, block := range blocks { + key, ok := block.Object.(K) + if !ok { + return fmt.Errorf("expected %T; got %T", key, block.Object) + } + keys = append(keys, key) + } + + b.keys = keys + return nil +} + +func (b *bucket[KT, K]) save() error { + var buf bytes.Buffer + buf.WriteString("// THIS FILE IS GENERATED. DO NOT EDIT THIS FILE DIRECTLY.\n\n") + for _, key := range b.keys { + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return err + } + _ = pem.Encode(&buf, &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + }) + } + return os.WriteFile(b.path(), buf.Bytes(), 0600) +} + +func (b *bucket[KT, K]) path() string { + return filepath.Join(packageDir, b.kt.Path()) +} diff --git a/test/testkey/ec256.pem b/test/testkey/ec256.pem new file mode 100644 index 0000000000..52c2a3ad8a --- /dev/null +++ b/test/testkey/ec256.pem @@ -0,0 +1,222 @@ +// THIS FILE IS GENERATED. DO NOT EDIT THIS FILE DIRECTLY. + +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs/CcKxAEIyBBEQ9h +ES2kJbWTz79ut45qAb0UgqrGqmOhRANCAARssWdfmS3D4INrpLBdSBxzso5kPPSX +F21JuznwCuYKNV5LnzhUA3nt2+6e18ZIXUDxl+CpkvCYc10MO6SYg6AE +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCAArepHPkJwmqERo +pfZl0qhRf2rjSBHr21qTiZeXDVqhRANCAAT7HAJMgJVxpRuOiPGRcGSz5VxeSl34 +45bHkNRlDu8MhRZCawM5ihRL1Fga/xQ32/XAI9/hUaYGUmgHNqksgUSB +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgV6cmzRw5HX42HCcX +snyrAoH2QIrwavkpv2iK7zI5ZeGhRANCAATIBgjdfKk1g4aO7iFzGFJjBMg+oPST +s7kdURwISvzqLL7AHh/NZB2K3ygHYSr21uh5bP0xNEf7OJkeljRrB4P6 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3+Vfx+cmsP3Xlii+ +GWjzD8KAH4EAxvjTmu5NxM9gARihRANCAASGuhII3x3nxcFnz/SCtibXMjUPtSqU +NpGg5QEiiRxUT3Cwn31MPznLbKCksm9pA9OLBxnTp+geBYc+FPNzpDa9 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIRLM8pdBv1SmIN9U +Dj4X274iAsgS3x3YqLnehGZIEXShRANCAAQHwL9hzYAZQao3Kq7BSgtPpkIizU7p +XKq8YMMuuCzLHH9dSUGoeY9fzIVNIuKpV+fGbZGJQbD2qJOB7eKnmNwr +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvHf/cn5uRP7IPgMd +EPmQwSC82vL7bzYD6vhSOnwNV3mhRANCAASDHYH4T457og/aLIyywEd0yAokGM/e +BGve8253yK5QYtB76IZOHGrGzfMxwbSU3GXnF4G3Pq1cPN6U+wRRrCyW +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg04FTEn6Nq7bO1O2T +9SckWTyHjXuEah5dYhFHlqfD2VuhRANCAAQIglnCh2Bv6RhpDz73Y7AfZ52gZI9m +pjK2LIVimFo+HiGqGLWMqrQrcf992968VTh9eqvC+5u4jaSuorbj/8wq +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggSbEyQACDOVicuGe +Tn7X2Th3xLMalD13eZXbv6mU2X+hRANCAATedLTufASz/anZIs5eL1AUcUdJz6w6 +t6+QlcIoC6IxT+shp2OPt8b7KpiEllNyfi3nmyXqbKFtaXlJPzIfeUxR +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwOYQN3832guOFAWK +2VHRxf9k6YBN7/3IvVnp1tY58I+hRANCAASfdlP1vbeaDaL7hDFQpGSoEBb3sEWk +fKW0dguBYS7ZnhwLWPLGPMLdy20pl5YYekg8wdb8tvTNTaBOdCAqOE/g +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg01wVqQTsEqQPM/Cv +5daJ7FAvGsa2OsgB6GGiET3DcUGhRANCAATVNzirHlWDqrFxJ5vj32+yZmetTAoo +QnEy9YZJJMtrKRMcGMb1ie7w6yw1OsM/SW238bHPZfCGqPXF/5zqXRt6 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglNQ+ilhINbPXiXp6 ++z9lhv5f2/v2y29x9YvezlclJ0+hRANCAASoHkiYYcDAT1+vY5k6kC2omUQxgAcx +SD8DLIBl1no0P1SBo4lnTKaRIXOdmwhC0+po1/WewhAwcAoKEuufRiC7 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQghMbLQU9t/+1o21m5 +pOple/l1/1JZYhaDUo5l2Qj7u9OhRANCAAQQVi/9iTzui2jUKp0vz8gpUy54SQJk +y+hs/WYKWZkqmRuuvxKMV/vuC/ZRHA43Aihs4eWqC6xULiFebv4g64x5 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgt+BJ6enqEMh2Jy1g +o2TXVHdfwlAr8KNOhw1zyIIACvShRANCAAS7En0X7wqBOH/JOWvIqtGe/XCuYMoc +K5RRo0vzxjIBiJBT1v0OV2dsQOA+Wq3G4vFlH3375MXA/zv6cVV8lj3x +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcKrCV3XwNkv+pr9a +//1sI7XYGZ5FwRtmhQ25N6FiZMahRANCAAS44PF0r2WhrXMIOrmD1Eqx2UTpTCWq +lu18Rrbvi7987+MZOMkRhJHHflmZ4r3X3mAPJcc3AgM7yBSRfclMPEcy +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCQs10kYNvq5wn4Pg +/6g4RpDaWdGMDjglXd0g4+R10DihRANCAASuht5hSxMmXc8m0uyKmAQjUhnRE/+G +8iF98O3ZtnihpMgDa9vtDYySB5fCPzYsy6q+U1cgSLpXgxD33cESp0Zp +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgMZ1ukUtZhk8OMNjL +Vux90wmhRpiQVxZGWiFPJVJCBWWhRANCAAQZr+FYxKBfbxKjXT2dyzDuYJsIqPr2 +5+Ql5Xf8VmWzGOEe2EGRhbjdP/UR8z5sz+bEqOxTSGHOmw++LCIxf9fD +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPezarwGukMMyk3vU +QV+fvX5XdZZxoDFKikgLEOGJbdShRANCAAQXs7VTH/Auctv/EZwxdVknY0VTta81 +L3axGhwt84qZfLZA/bkcGLMTWqwEnnx7SBBa0zjicn8pOoxaXYYx8K7E +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDHmBvYkgP5aHqwHY +Uzx6B9TXkx8YqJeLjDs+KunDIrKhRANCAASpt35K6QAu7vJO6pB/sYzDGmqF5My0 +2RSCDFihHcTKzDzrZTOBVPeZc0wYPTCFpQ3bJmDy0EzB5acLdyhnjGi2 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg298Bj04e3u1Vm929 +x9ay217Inr7v6hoJH9YL6745xRWhRANCAAT+evK/nRMuQ6vtdYiQXREY2x5uLlBQ +YYHVaKSzf/kzQSz6ggZdk/9oNdL2iL9Ul7jMzvjEvye8y0HzGAv3fCfp +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWWwhVu45jPCAKa0w +rtjNDdIclhAtTvEpX4e6Y+VoFm+hRANCAAQkwnGHJiC/IKxjWrwEJoNEtl3aiCoH +7Y+q5CK73MfDSFDRJuPXWYVvHhujje8tk3J9wCd9aHVeNws4QEX46T/o +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUO8p8VnEsA0fcuW+ +Fis/bmjcPYJIR4VSDz6Pkq0792ihRANCAATi2duWJ4iSAjA4F3mSQKd3QugNO4du +dH2xcEnNpkURro4bDM+resgq8ezlF5/ERdzAUk1RNwTBaU9yHvogQ8+A +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPjwFlD1cQ5KjDbBH +TIjV+Yk1ZpHM3c9nllI1R1uCbp6hRANCAAQR1qN422VuofdtCL/YpHTwHnU4mays +THmQ5a+wSIqwuNCI/p+WzQfFaLQhBTVSdqCOWKhqflSqY+cO2iGVZZkv +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg6hW9p61/L2BeaVZF +Y16WpFA6gzbSfgeH4Zu5s2Zj+r6hRANCAAS9G+rYS4ZhaudCCOxkUYm5gRm/uV1a +tUmjSpPZOxAtI4k7rFS1jAxHGUuNByGe3X0spw2RoQR49ofmIVBfN58+ +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgoljSzaTSojp2EgLB +X1hoibFQGGmSOswQxbWaYMWc9SChRANCAAStsJH1aIGyRPbl98O+riAjK+YNsh1f +Fu9WDM07tBRrskrJnjSN0AurVyhytbobswniVcOhS5bWlLukQZ5RT7cK +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9bgY1xti7/auMvrE +qpZG9GG+M6ftq9hRDNeUPAv24RGhRANCAAS0txtop4q6q6D3PnzVkSoPCf1HiQzz +NIZfa0YFaGv/YQGziZuIaKhpliSBEIW3Ee8OEBNnkpWa44B48MvAd8NH +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgegrcliR1ICIdgHkD +kYVvNRdM+j8kC5vxPBR3Uxh5R76hRANCAAQcQoV59skUMnFH+0foDTlTzvSWFLGP +9l1RAfXZYKnLYMSa3MZ5hmJYWzKZ5TjR63b6xIm/z/tyjI1UprwBoUP8 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgoaHkvQjVmxTQq+QK +OyzO0Qw+HAcJavaUlRODsox+V26hRANCAASAXuN4+RMbohgTC+InyOQ8mu8AKEqj +OkfN2FzjVDbHP2IjyupsN1bj2Mu8zxx4AwnquTf/gE97xqbF5zhoUHxc +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRiQd6++0d8fuGm60 +fbRGw8K3O7z6xHdjMUWkcYYCXjKhRANCAASiJrHkMOsc2l06sB/NP+CTHCFts5ea +79wX8/1fbroymm7FRh0CzYYcP2hP5MFyxlWc6lxV0GPz9p7/bziRM8lX +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfP9spzAt/sXrv6P6 +fZ2V8AOoNh9fzCZPbMDhNO3dwAihRANCAARYat8zfxqf/1PXgy7cv2R5zFTZD1GJ +6zzInq3DJ1hvoza+EWfD7K9lNZRvdpRjF0m2HsTAyJ8/fbpbfAsis3TA +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUIT5iwodfbr0JWKV +q26KXRaQIJQ3h6pGivoAg2GdV7GhRANCAATDG2Q4Bj5a9CxL+TZsc7F2G3vyqyVv +DSkBZoGsDIJFATjkj3w7vMETOg833/ev/K735Ubd7Rt43gtep/AmFCYV +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1hTNnSgVLoGUU02/ +8WnIACBPbKHo1e1V7WTifddwfAihRANCAATXVo6C542OuhxjZmg+C5sW0283SJz2 +lPqeGRc2sdqB2wEBBOkqXsPKE10ONYg1UOPV9Ye/u4g+4H5XoVAdMgYx +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgexAx/7S+6djaXKIp +RcyFpA/6BY+IovMa/SfKIjsMSWKhRANCAATvschwitvaGRgBjJxCGvfjTMGrZ6qg +zvLTuBQHFicda8tiSuexw+hpiSnrv1cX7u5TgJo3Ecr9RjwBi8Xz55GU +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgoYLwhq/afAzm5oA5 +ArZQub+EpIZIuAPwWAUlkexctbyhRANCAATCaf6reaIOZbK/wgIZr1BkTmz2Jk/F +nEsomBCAM10mcXjbTX06ko8w2nmPbvdI2dd6Vzw5oDLvRBRL2xPXy8Dx +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgi9tucdEzl5t85GMY +l27paU4Z2ldWIYEjWP8eegyKKG2hRANCAATDeSAPDA4Rhh7Kgn6T1XcKoIljfQvh +PP+xHR6gCyXuKR6fCQUJwsTz8egSmtaHk2xQSLSwNo9Jwgy5spaxzaXZ +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgauTNUvkbD8ijYlPR +yuTg+Ul/TgBAQqCWl59H6asrbAWhRANCAAQMNUKoLptNj7UXK3J9AOBlCTFiTP3R +JuWlSKdbp19pjbTrrRJ0u8wCohkl43Zcw1ArBzVLTsnJW5knk1DbHUC7 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRKUbUj2iaZCgCeOv +5s4l+Dg0KCNvdRs6h0dGVht0Kg+hRANCAASf4W6eAJgxOS57Jn4W9Cd1Wae3+2jR +3+lG6ni7x9xWk9OLYaiWe/+n4yn4QgBAre2Lmjxweq/mhynBmhf+LJUW +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1xH8jTPXbnkW2dwp +JRn1fo6O2jWh6dX0myqGIPJ5S8GhRANCAARvvjo0/1bQZjFSqS6zSoag61PQmodM +fiiQteFV07/vhcHeTRU35MMu7QS3ENDZP7UpLjVNGoUqYd2+kZ4SeQS9 +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNd5koqqqfc2RFeMP +lCtJHpVAjSIfHi1HyVUgRlzLKzqhRANCAARdYcJptEArPC5PYPlqnv5FHlDnj+zH +WmlosM36oCGq4029bZFAXnz1uxHpGFsQL2KTPiwnyqD/smBDTjOWIbZF +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQBz4ZYX9VFAXp8/K +4E6yRev5W6KN0X+YKgFch7TxDEGhRANCAAR7C7O45csLVXzozjoqxDfQaBK2P6UZ +wkGy+NNh0M75CK5pUVfnjFx/y+QVzWrmPYF7qLhquOxv/qatjhk8+n9q +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfOgPGqv+h9+MZb2W +B55uO4QjYTQtw0Me+3MFeXrjct6hRANCAATmLvOq9++TtdKRCz/L68iZuP/yQFJW +gAR0c13OQIyBufebxDemMD76Re6UzwmjG9H1iz5jIp4BUI8ZMb6z9uQ+ +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2W/vwkUdAz1Di0pK +II0LSHIFkDcOti75FPsfht/+FdOhRANCAASGANLKu2clEI9XIEDE6GhvtlOe+Nro +fkUqew1tPHw67YdlLoNbpRF1V46DatBMdNNskeUPTbLv5VpOEoIjID+I +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFwe4zneot+8dLmnu +nh58hl6Uv6+/DteD+pgOPkn3X/qhRANCAAQnoDwjh/WT0iZEmwe68DWzJJAvi+EY +C35bA+9JoBl4GMno+oVavReizY704aVul952NpJ9yJVFw2uWZmDhn5qB +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcMC42hB+XRKA6ZtF +w0Q0CuLBukTqyX49XOQq/45rb9yhRANCAATYPDZJ95YtsilzwkcuP6Nga+hDAUf2 +tzAlmSuDNzJoxlB5YEmGRji7/STjBK9P3qdy5vDbCVcWQNhc8fBJRv3x +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2Uw/wBfUIY2YKQF/ +08Cxrmq1vEjQe/pofhnSgD5I7kihRANCAASyL5TIaWoiIaQV+1H80jswPHRU0fzQ +5802QHn0te5ugJ4izcn7JqPss47zkud5X9iuCUwZ0QvvQJBUpvuNTwHM +-----END PRIVATE KEY----- diff --git a/test/testkey/ec384.pem b/test/testkey/ec384.pem new file mode 100644 index 0000000000..0679baf1da --- /dev/null +++ b/test/testkey/ec384.pem @@ -0,0 +1,32 @@ +// THIS FILE IS GENERATED. DO NOT EDIT THIS FILE DIRECTLY. + +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAo6qtpUSdCIrwS+16g +MtaRsYVf2Z7fPRT4ukHSmMtLgozw/es9VW/tg9QK8GU0LoehZANiAARLpZpT8Qum +17LOc8Lu7xKGndoCQRE01Kkg4keSXq8237rwjUALWMkk35rSh6CaXgbladjUmAbX +6TOHdN2Tn1YKZMzDNI+ENji/wn4bAatn2qahEACUXvIhrNn+jLQ7S4A= +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDATer/8uq6Tv57VBmBM +RVHH8Msy6RS8RkdrNJsPfjIjSP3T7MwmsDQfrVu5WGOCa1ChZANiAATBbvC3ehfN +zZIYfxAykshZoF1W7AUnReX3L+lhI9fGh9svWLwsov8NfUBl2Fp2qsalhbHpW4oW ++P/mkAo8KPvhznfAtbX1lDHLz+TJzyca+KCXA60lmJoZAb5IvsBwGSU= +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCTilaAMDtR7sO6TOOQ +DNiqNk4ZAW2wt6QyIHStEttR8y2sJfw48FvNR9EQUO7H15WhZANiAARj+OSAnjeO +U0ugQc7AOr0ilCamuGmO+Sf6KdazT2fgSQ8ccaMdxmLhEe+kqJpx/uxRqpZ9t5P8 +TuPpIdkQSP2xf3iaBis5WDY2qql2SajLUZWSCKcHPsz8KNCk8D5udGE= +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCW3h6M/vAAeBLmNQ/B +4HfLII2o0fzqcj4V7VFTYUIJNmJy9o7aNzrJGlmET7MHQdChZANiAAQedectkFxe +mS+35TbnInQSWS9kU/4YtMYNlqnShT3HWM8cwghlvjUK/Yfawhhi8RhRx7m/U1t+ ++WLnQSdv5oKND6Ast3P50IopE7xWNRbw+T/dtTvih/3PatVivlXhnH8= +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDJksMQHQQfgybDbkIP +SDKRIn49ua1EAT0cigIQ/jIdgQS3h81JB0l0jx6fi7kR1eqhZANiAARYSu09r7e5 +wKpWaFBscqgkWuK4jJjtCCOU6+f9Z8Hg8275u9rKI2QVzzq2X9Cutz4YtSNnPztn +ljiDXG/UaSGqO1cvjAcTPgrQYlWguFiZRkJmd8DR3sY+iTfLvlI6q44= +-----END PRIVATE KEY----- diff --git a/test/testkey/generate.sh b/test/testkey/generate.sh deleted file mode 100755 index e11c606f70..0000000000 --- a/test/testkey/generate.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# generate.sh - regenerate the test keys -# -# This script regenerates the test keys used by unit tests. It should be -# run when the number of keys used to test a package exceeds the number of -# pregenerated keys for that type. - -# The following variables control how many keys of each type are generated: -NUMRSA2048=5 -NUMRSA4096=5 -NUMEC256=48 -NUMEC384=5 - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - -set -e - -cd "${DIR}" - -cleanup() { - rm -f keys.go.tmp -} - -trap cleanup EXIT - -go run genkeys.go \ - -rsa2048="${NUMRSA2048}" \ - -rsa4096="${NUMRSA4096}" \ - -ec256="${NUMEC256}" \ - -ec384="${NUMEC384}" > keys.go.tmp - -mv keys.go.tmp keys.go diff --git a/test/testkey/generator.go b/test/testkey/generator.go new file mode 100644 index 0000000000..4c8360fc39 --- /dev/null +++ b/test/testkey/generator.go @@ -0,0 +1,13 @@ +package testkey + +import ( + "crypto/ecdsa" + "crypto/rsa" +) + +type Generator struct{ keys Keys } + +func (g *Generator) GenerateRSA2048Key() (*rsa.PrivateKey, error) { return g.keys.NextRSA2048() } +func (g *Generator) GenerateRSA4096Key() (*rsa.PrivateKey, error) { return g.keys.NextRSA4096() } +func (g *Generator) GenerateEC256Key() (*ecdsa.PrivateKey, error) { return g.keys.NextEC256() } +func (g *Generator) GenerateEC384Key() (*ecdsa.PrivateKey, error) { return g.keys.NextEC384() } diff --git a/test/testkey/genkeys.go b/test/testkey/genkeys.go deleted file mode 100644 index 47da542ad5..0000000000 --- a/test/testkey/genkeys.go +++ /dev/null @@ -1,106 +0,0 @@ -// +build ignore - -package main - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "flag" - "fmt" - "go/format" - "io" - "os" - - "github.com/spiffe/spire/test/testkey" -) - -const ( - header = ` // THIS FILE IS GENERATED. DO NOT EDIT THIS FILE DIRECTLY UNLESS YOU ARE -// SEEDING A NEW KEY TYPE. -// -// To seed a new key type, add an empty exported []string variable for that -// key type and adjust the code in generate.sh and genkeys.go accordingly. -package testkey - -var ( -` - footer = `) -` -) - -func main() { - rsa2048 := flag.Int("rsa2048", 0, "Number of rsa2048 keys to generate") - rsa4096 := flag.Int("rsa4096", 0, "Number of rsa4096 keys to generate") - ec256 := flag.Int("ec256", 0, "Number of ec256 keys to generate") - ec384 := flag.Int("ec384", 0, "Number of ec384 keys to generate") - flag.Parse() - - buf := new(bytes.Buffer) - - fmt.Fprintln(buf, header) - - writeKeys(buf, "RSA2048Keys", testkey.RSA2048Keys, *rsa2048, genRSA2048) - writeKeys(buf, "RSA4096Keys", testkey.RSA4096Keys, *rsa4096, genRSA4096) - writeKeys(buf, "EC256Keys", testkey.EC256Keys, *ec256, genEC256) - writeKeys(buf, "EC384Keys", testkey.EC384Keys, *ec384, genEC384) - - fmt.Fprintln(buf, footer) - - formatted, err := format.Source(buf.Bytes()) - if err != nil { - os.Stderr.Write(buf.Bytes()) - panic(err) - } - _, err = os.Stdout.Write(formatted) - check(err) -} - -func writeKeys(buf io.Writer, varName string, existing []string, wanted int, genKey func() crypto.PrivateKey) { - fmt.Fprintf(buf, "%s = []string{\n", varName) - for i := 0; i < wanted; i++ { - if i < len(existing) { - fmt.Fprintf(buf, "`%s`,\n", existing[i]) - } else { - fmt.Fprintf(buf, "`%s`,\n", toPEM(genKey())) - } - } - fmt.Fprintln(buf, "}") -} - -func genRSA2048() crypto.PrivateKey { return genRSA(2048) } -func genRSA4096() crypto.PrivateKey { return genRSA(4096) } -func genEC256() crypto.PrivateKey { return genEC(elliptic.P256()) } -func genEC384() crypto.PrivateKey { return genEC(elliptic.P384()) } - -func genRSA(bits int) *rsa.PrivateKey { - key, err := rsa.GenerateKey(rand.Reader, bits) - check(err) - return key -} - -func genEC(curve elliptic.Curve) *ecdsa.PrivateKey { - key, err := ecdsa.GenerateKey(curve, rand.Reader) - check(err) - return key -} - -func toPEM(key crypto.PrivateKey) string { - data, err := x509.MarshalPKCS8PrivateKey(key) - check(err) - return string(pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: data, - })) -} - -func check(err error) { - if err != nil { - panic(err) - } -} diff --git a/test/testkey/keys.go b/test/testkey/keys.go index 5c673f9134..6a407eccae 100644 --- a/test/testkey/keys.go +++ b/test/testkey/keys.go @@ -1,750 +1,190 @@ -// THIS FILE IS GENERATED. DO NOT EDIT THIS FILE DIRECTLY UNLESS YOU ARE -// SEEDING A NEW KEY TYPE. -// -// To seed a new key type, add an empty exported []string variable for that -// key type and adjust the code in generate.sh and genkeys.go accordingly. package testkey +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + var ( - RSA2048Keys = []string{ - `-----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDd9ubsnFhQErLy -XBIkk2r0d9jbnY4h4h6Q2IbPVLluWDKl9Y8QZgPYAANCQJIIMbkMe0A33d/NIiTd -uAwaNpOuhhECXL4hW/Z+RTSk+hHwmjqbSAYg8a+iY1xoOVgpQqKVnHOjJy7/f2Ce -BkgjxH/PcqaFd27Xh6rrTeE4UvMZ/wqLuGoAdrJACVmW9Ibtb09dI+SxndVMkSIa -GOMbI2x3rmdLmXt4+zIUlczTSe6xJ/Ym/13LcDn5CHAEW5uleuCRNhuAsBhJ7JUF -qoWnZfTWl2JmtT8CsvrJIVQwuBHHD2rSsSczJnk4XqTomWrgkXJ+R5b1vueKZgRl -TWKAf94NAgMBAAECggEBAIbja7Rw0s5efrcAMtpdaAsG5heYaO64bqDRpSNMNqAT -IzxtvTJW/JEAePqDKPun0+/82qrYwA/2ZvcCyQAJGLhfquiHmdfF/kcAIZz8h3hz -EZiaLXdJyNVjkp2X87anUwyeljuZLVuBeiKuaJqr6IwV/ZJwYUDDpp/2pR7Icgni -LNr83j1LLe0P+oyQngMbfP0Ym41Qe3jGicrThRWqhpi1YWclSQqhv6dWl+gDchYg -rhENSQqv9EOs7p7p76mO4j4irPaOeAcKldb2Qy2w5XSNQpcYKeE9dP+WyypwrDJo -tjy4Oe/ytZBx9RSOeIaOPSjtQzAQ0vaRQNryDYp91HUCgYEA4qdizLqOIhpLzHZi -dInUcQywZ0iwn4FNBEJpjYpaRvTPDRpwuM79BJnBLFmqikI/Ux6zbKTJTrRil4w+ -qWi+d4H+lBCt57cQ0TMMO+Wx1ll4yP8H4llDnEG1ShQpzqJozKAhTjEv8PqZ1Ofm -Yup2lAV0VF8vpQ2qAbnwbAKd6+cCgYEA+rQVLJe7TbOifiP57A4yEAD6+9a532cY -XYU4fBr/sO9kprzA9oy9xpObsBWVol/Ps8C6LZxtuy8lRLU5mqYcHXzX9BXMS5lq -1EmILpfTdCSKMx+hKyilPTah4V09XUPloCLMRxOBurYc92bfQqCxoOSAQ3xkV1Lb -HKFHTEyjB+sCgYBg94e3WuDQCjv/f25juUvgCbO2nEykEOdoORl7aoNw8+9ZBcTP -7A4nV7hjRwFFIU6COxI8GsvdFiNP/roYNC24Zy8JHZGpHpLdTV+giIZq0+Bu7Vzs -I5BfW2U6CiJQuv7m/GMDVtvR1wTVJ1lTXEfaz/KS3h4+GPhwwKHxM7VoZQKBgQCL -tNjr3a4yEb2mf5PhWNPLGVDEEJfKB+CLFRcyCbWdOvqi9CKwAiQrScyDprOZJb1Q -FUFq83Yuzon9LORp6tFWGWEakx8ird7baBKsrEzYtxgQfzrIG0FhyFUTimsM0y5e -O6YMobE0hBHGSJx3u6bg2xUjBmnAQ9r4rGNYAIkFewKBgQDHEsehZjp+Yo9mDO6m -FYeZcC7UfRmFqKblJ3wB0CrDCaQj2NJZmDdc8Z4o3c0qzHtBTQqE50Sr9cvw16Mq -ZYzi/WHiVlxyZCdXQhdgO+PNwfNVeJfaHfMClLMQ8jtyiki0xzajGLliZITW8J96 -gXMlunwAVud98zbPY3eTlCxmWA== ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDXwSX5zpH+ag9G -QiihIUsjAZ3lCZWIAdGbkb9kiyM/gWzwpawkkRmFM3KMrf4+sv307TZEwmqC9LCo -F4SVbnJ/1hVey/fSM7cvcb4MmFhK60pLEwxFNT6vQ0iDuMDlmsge+1MxyVrmaZf4 -MKnG0+8KYPJ4Lq4AYtwir9lh8mNjAcOCZw/b5jLBQcETgbPdDKqoyFjtFK/CiakW -rptlD/YAnq28Qxhm/ylGYckc7Y4stfrP2zosg3fFQkG7nEuZUluSRlrXG3lA++cx -bbUzjPFVYsQoFybxOP/1dqBLIB7h399srFcrV5A9uNXUH+C73P7ic3Rgn6RvqKLg -JInhAuvrAgMBAAECggEAUiDJBu12L4VJ6TG365YT1vB/nTbnv80JcBGr7Gb2dO8c -fAJko8rEDytFWH3HATD7cOd3N/dVuiHQuTuykXtohHcGzX4RCOf8vEes03iOa35j -Wm6WxiV9hhOzn8iNTRl9QiXjRllii8D4Q1aq2e/E14uN4OfL/oOjTmTn99vfr0jC -mIiHpnpocwO84LWCLtYRWUAHQAi30eY8fc8IbiZfYGK2mEM2PloGG+T2R/x5HDxi -d4nhXbhTFg6eu+0YUFry1EUfhGrab6h3ItxTjg+bw+Jv5VfI/91Vu86NSUQYsG7C -62D1/BPxC27YeGiOqqiMtr2MrY8QQyErckp2kKYEeQKBgQDxl1ocKNAfRuYaRrAq -o7CO+cnEQORSLwv0sghOKq0s+BxSreJcMBgPQ8WOmfFdnpTMX9UyhaL07uFeTqRJ -Mv81BUZBBYUN7I0/at2Z1CVDItmsCK2L5JN2gRh/mbkdcVRxL99u3Fg5h2e5hPnB -uxHBbQ/po0cmBLqrpTm8TC1nZwKBgQDkn1FLGU2P6FyzCaqvppqXDrEULz38AEPa -6lL5SPhIjFyP1SZpl17pwqCjqhW9jgBbamgkvN8BLR0kdYTB0//vpe07TrnS5X5R -Id4FKYoUY6As1+wR8hiV6S9ARTAwEulqFx7YBdT014unpqPt8H77LMYh8grdkA1Z -vP8CiokY3QKBgQCnrxWsVeeezecIdefwsIzrsBSLUz8mi+EQhkGdf6GThOKjwG1M -71TDw5Zr1A3jnR5KfHnOB1OEDgn/GzaMWAkrE+4fU2V2tKmVSudkzgrO/nF3Js7O -Omjf59rJNjl2ZiLmLQQ4Plg+Fe24psNz5BP+3WQeFmZbzQyD9rqMJ5OcIQKBgCoS -F0eWlGtS+xwHP12rbu81SOjJ+MIS2mnCjRpKj0Xqbm4Zb0QnEtQ5eI4lknKbWv13 -i0qXZwI0ZxR6e7+fX42eHxW22wMwMBqF/PE+P6aY3rTh1xNGVbfgfU16be+qy0E2 -l/pwuEuGDrD/PVNf2j4mcx90BwPWql5FJTg2fhwRAoGAUyY4aob6rvb2nh8Beryd -LzG8bRGH2bIwGhoCbPEZ5S9bIt9PD1gdco6K5uu+w36CGSh9G7wsqRT/92uZrhlv -MFz3mtJcSF1oXxngcDXBxklqCYevCAvEMmFDRahFecm0ddpcOPpXZN6o9P3Btb+p -0m4dsUV5gSr7k0ASgzlfUZw= ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNmNT3nUz9JmxW -XBum91GWWqqpuH6Hei5AYVu2SQx4Z6guXTeSL8Kw8lg/NlnDPDfRJ8Xi3rBwGSsu -NvPg3nIV+QbkhGO6eQPqk1Q7bwYY4YcNNy1QsCrqbRz+C6HB6V0jrVBs4W8F+Px3 -NaygV9gakmi2h/kHI3+Xi6iqVt5FuHHIlo2srl3LUvF71nfYTx3ptkWgBh3DMn1y -N39N8zm0+/08Vu2jBb1O9m8OegcxFejFPZauG2/t2wfLpuBRhetSUHYRCOhE8nxn -/a/ODeTFv62qQnKARAYHY5+qlX3GuW0b+97U6B2GhtMCl6BCyo0bapGSaiP+UNHg -wJiFPKTfAgMBAAECggEALHZZ0DbveGu/0ClZPZGMzmRLNisVCf1tVTT43YIMtVlN -cMFuDCpSA2xVk04QuX0jYRMl43tfUs7OnM77jOzuZTwWtoK/Aou7Qhach8Hp4qWb -TEtbfHCsx0fTlkkzsTjjkJyhoPpbxUHkb29nJsH4lT6GcLsVKusNY9urHTNuHEt0 -s3EDU6zdmJNC1KIAfCRbs/Y7/80zFK/v6HOrTpjsiObPZPVy2UBoYSpUJ3NHm2wd -UaKlRJeAzEWxIEiz/fpRv1qQ72d4OZUZAGuQf3ua7dMk1KGsl9nuKt4/cJNRhYk6 -2IziPKPS3j2Rl3CeYUI9y0ptd+q3uaOdC3aGkHFymQKBgQD1bYF1SDiO0ININpwJ -StPvf78lfkinOeS67tkiwTBB5VThr41LDT9S0u3tK/gY/5TinmBqJ+8KvBTi1lJ1 -acrAAAWgiOWxCQs3AxoqQlvw6zyFhQI7D8kDVa5TuF807WX0aQKsrefcgn2k1uFb -kYAgFz5hpgNxAITlBs7Yw72h+wKBgQDWdBYJ6XN0muI9fkpu4Aw54Lo8y94IKIrk -AxilAwffxeksySfHx/v1vHMNZ1TYalVtARiMEg28dV+GJzQUqZcoVDgm+efw+Eoi -fOtz3JPDPVS+DOS50rsrEUmaNtX91oNGq4Ab0U+zqEnY4SDJRgrYR6sQCXE8cNAD -VnmdYil3bQKBgQDJMX0IBS2APgTxRPtDTtlQo7ux48VxeS894yGTsSV7T6H25TAC -D5kSr6GmZP4cmCCYalcFjzkR5r+EeUfdwt0X6qyyPqd1KsHL1joz3HR8morhtfjv -K/CQUEP5k9JQlDCZXSB5KJNDnKGdaR4TL8MGC6fy6uI1V8SZ76vP3R7u0wKBgGdm -W4X18LwlPbZmoR7qmhqB92n+5hRK8ATWVViiqHZFp5L3fl2+WAb5EQRCcU9TndLL -93j573ORqDg3yM25o29HhDeOwT2Xia8tSh14GirF9IkaEGJkb+hpEnLvw6f7eRpm -8IL5HhPCrbCLg9JoGiyECb/Wvallv3YMiODQhqvVAoGBAK2xCRBYiESYJtDWMg/0 -zQwym38BL0BnN0nI4K0jSk3Nb2ERsKI3F5ESYw+sH8OJcNCrT9yFh046JkdlmJF/ -C/DYvEe7kCKskrTjFVrEszlPHkQ5pvGV+Nhzb2oqqwna/M66uYnZM0Mtucrr6kY1 -1jgfW03PDLuwyn0X4mz4zUnK ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDrDcWv3DXVEt+9 -Ot1bcUcELKMU2tUx9uV2pxJPNkbv5Yhe4dzCLW7XeOFeUA28M9SvEYZB494Q96to -sb3PnLAqgMcrvF+qV01i2C9zUlJQKmgUB7cJucMIBlmWOmIktEAP6ynLoPvZNMNK -GsSSBevRg5XmfFHm+y0g0f7jUIpKRmPjGs5Ja8yTZ/tahT4EbXe/KIdnOzLUOLeI -fqv9tNZG7rIseGrHjo+X380lGQlUPCSjHHOHLuqio0varfbaWxTHgOJosKkNX4Rd -xyxiAYyD04dhBqFxDN9KHAJTmTEBeVTcquBnYYlpW6M+wQkdBxRM9n+2my/Ju+kl -T0fFPxXjAgMBAAECggEATJzrj1t2TGHffT0fgzuTyx+FdDmzfZqcxiTEif8F9hFZ -dpulqcDHPQIQav4qy+oU648JtJZB/kPLoc81L+NBZEkfIfk/MTVNaudN6Aev/qMj -00uEdG4QzJ/NOXMEJDSAIPAkalOpYu2YraG16mZ1q495H2vsAh5iYi0wKAVLXdOd -n5JsPB5GBSsD3p8vR6iiTlLR6pYdpzr9X0zVqvWQmDxcgy+Sq0/PEmSD2WB29I5D -BIzuAwqg5UeYddLFgjkd8+zO3GpqC8WmEtxmQPnL4ACauU3iyhuANWZAtBvYnnxr -94us3dgz6pevxy3F6pIu4Y1TFM+biIDMD5qNlR7PqQKBgQD99S+gmZ+hdLGvAPaD -bTzx/W1Ab6vpm9GoxrDW6+pXKZ3YyMYYJeckEzVZzPVGxSCutSJ5HL6ghZKb/C7g -xq++GnJggxTkOo2Ppi8XWUsp3hYhA/nyBOhyzk8w78sgtoKcqILNcWjQN+cNy4lT -NsbCIbatVM6fD8MvRJEwMvHlDQKBgQDs8atT3NpeTOA1GrXb+y1Jf9n6gFaDRNWw -4RreaGlwlGr6crAnw7Izj3Tv41juWZgUha0mrLqeLHKE7H3S6n7EKl54Y/s1IKzC -DYFGPNsw/4ut+9MG4pTJR8tCOy7ad3YoJqRxnq0LX4EjmW3wZ0ZU7bTnAXKhPV1/ -IyQdEHUKrwKBgE2f5C7yxhhT0vvrD26ctURCcmJ/v8xoFG3CTctj0P1TeywIMoSv -ETe1p1kLjO1U0+iS9TaP0rS+H1IOg0WxdYZmDw/xATHBtAN0iHBamt7xQ1JUJNIV -Lffpl8sdgLk/EC1SVKj3QVJjw/wzeoY0+Avewje49G8qIj8QdlCFQesBAoGAZsWj -9GoU5VYe4anGO7ZEvF6SI49K9wECVwgsaU+MfGJDzIG2WmkNgEO3Ct3nkuqVhkE9 -C0tcXoMU4QbaxIMlnNxrwXhMW4ziogDNk7ONt0EASuSxcYkR1AQp635UIjoyq9Om -/AlBMW+pSdGg1+dToD7CengsSjeduCl73odm3M0CgYAifHPcZXSBWGONhkzixhQo -IPgJg1DwiWFw7Gg4nl5057FIUpY6gs8/SPh0e8hA0xc7P8gvHLTwSn2D+NlqdCYE -ciyHaFafp/aeKMIr+euAbrVJqapN9oKRORMbKjYgd6jrZgTfdFy00amKOIOpi2PP -OdC446+kQaruQKT+MmvASw== ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyhAPXGbIsrgem -sY9prVXC6kM/VuuZSDUMUzysSxmSEq6MDmmrDsn3yj8+fmeg/GL6Cw9crLUn/asj -wL/RIJZ6TiFLloPn/Qmd4i6j2Gd2dXV+VPCxbI5745wNJsMPA61UDy5HKtyzffjy -ZwINsn6I0O0QjNDLcOhzzTZrl+XeWz0V2BtDpRSXtFnJFZQiVNnR7YXEEkuDlfLg -Ato04geVd1WvUihGjne5tNAeG/iVJT+iY10OMKbLqRrxLLdnv0PAbZVBAk+H3NBY -YHr7AMH5IZua+8sqkL6KWbG9dcfavj067iYW6WuQs5YHawMu5LDPkUB+uQhLPz7T -Q+ao/topAgMBAAECggEBAIgDyiFENNuSJcY+l7S/Gw8OCvrhsVTzDWg8q4fjGLKR -hWi1OpHZDM29zX5CNZcVdhxp/ORxuv2ja5gsLnqax8ycZwX+wrYpuAAR69Nu+TXq -vQDqmxG0UsPWYnoqxIhWFuRrRKl44PvpyZp4Hbpt+7cm6NmpURURDCvfec10yPIn -b5JPwmhPhzhlXQXRTXJ3wSxTdyHDoBjh9ASQpUs97RRcIvvhJsIevkGSweggN+0y -3dl2JcnWzJmYOCDWu9il2/lDN9avl7O1JCItrbVs8znT00JhsMGczTby+61ZYP06 -sk4p4zNRCKk/groj0rO33bpNvylgJXoP+CKEbqgCerkCgYEAxXKwm0iQVUFGZmp6 -6EoABeCRvNH4Rpi1qmHwLvfruygUU9JTkMq3VR+NJzXTnzmEGSGmtJr7bt9xw75Z -Eu1hTPUa/cH9vau9hXG8X1BcaK3f/SD99SN7lTQZSfaYhKURTi7iiUmb2S24Crez -3x3ZiQkWUdVtzLFzu4eGgd1db0MCgYEA53QS0Jqdug6z+ZufDaT5uiJd/gL+3lnx -L8dfPjZ/Ady0ANkbk0C19oqF+XcAPFVJ6w+7ldHF8eD5XqMeeCJ3Cn7q3INg5p6B -IfOp++Kc1wIxytJvBNvwJFhAAJVitecQM1yoXI6xmFzun/a7fovOZVDvtmeXwCWq -kTe41cwgjCMCgYALknI5V6Jl7MJ0hC8Z6CRiM8w21dOIR7D2AHF0P0GIoYu3ce9F -4CuoiIXcU3JItbVBR9CeayrrT5s6TrCnxFPcj8z6LGFzuVoNNSJGL86KsA4dps5b -jK3Ui84joJlFxOrjuym5xB+nNd/AeQ3IuNYkCu1M9IZP5eKTjhjbCZ9NQwKBgQDm -cYAmKDtwOyFgHVywNhjaBUu2E391HPHxUzz18UZlMTwbOA6nfx9s16Dqr1wRtg1B -t8laMqE14XwHiLtWe2Iwlgr7AOei1h/WEQemnYrw2+N9gCU/Hkgrt54JtrKwT92m -ddO/S+dwvt5rcDpflY0q/Pmej+fcTORVb7hdTb7+JwKBgDO37SQ9f7fSU7P5PXKE -zBB/eVv09lnoAfFFDu3wz6//Z4BtyTuJpNvfNFBhBbzc8sigCgBp6ANtgd6HHOgW -MTyxR2dpdn2bV7iCEXVrNBMDlnEZK8XWwR0gSWx+M8LnkpMpMU9wzJEaYLRpbrp0 -JjVArGW5gAB7z4R46Puq/7rx ------END PRIVATE KEY----- -`, + keys Keys + rsa2048Bucket bucket[rsa2048, *rsa.PrivateKey] + rsa4096Bucket bucket[rsa4096, *rsa.PrivateKey] + ec256Bucket bucket[ec256, *ecdsa.PrivateKey] + ec384Bucket bucket[ec384, *ecdsa.PrivateKey] +) + +func NewRSA2048(tb testing.TB) *rsa.PrivateKey { + return keys.NewRSA2048(tb) +} + +func MustRSA2048() *rsa.PrivateKey { + return keys.MustRSA2048() +} + +func NewRSA4096(tb testing.TB) *rsa.PrivateKey { + return keys.NewRSA4096(tb) +} + +func MustRSA4096() *rsa.PrivateKey { + return keys.MustRSA4096() +} + +func NewEC256(tb testing.TB) *ecdsa.PrivateKey { + return keys.NewEC256(tb) +} + +func MustEC256() *ecdsa.PrivateKey { + return keys.MustEC256() +} + +func NewEC384(tb testing.TB) *ecdsa.PrivateKey { + return keys.NewEC384(tb) +} + +func MustEC384() *ecdsa.PrivateKey { + return keys.MustEC384() +} + +type Keys struct { + mtx sync.Mutex + rsa2048Idx int + rsa4096Idx int + ec256Idx int + ec384Idx int +} + +func (ks *Keys) NewRSA2048(tb testing.TB) *rsa.PrivateKey { + key, err := ks.NextRSA2048() + require.NoError(tb, err) + return key +} + +func (ks *Keys) MustRSA2048() *rsa.PrivateKey { + key, err := ks.NextRSA2048() + check(err) + return key +} + +func (ks *Keys) NextRSA2048() (*rsa.PrivateKey, error) { + ks.mtx.Lock() + defer ks.mtx.Unlock() + key, err := rsa2048Bucket.At(ks.rsa2048Idx) + if err != nil { + return nil, err + } + ks.rsa2048Idx++ + return key, nil +} + +func (ks *Keys) NewRSA4096(tb testing.TB) *rsa.PrivateKey { + key, err := ks.NextRSA4096() + require.NoError(tb, err) + return key +} + +func (ks *Keys) MustRSA4096() *rsa.PrivateKey { + key, err := ks.NextRSA4096() + check(err) + return key +} + +func (ks *Keys) NextRSA4096() (*rsa.PrivateKey, error) { + ks.mtx.Lock() + defer ks.mtx.Unlock() + key, err := rsa4096Bucket.At(ks.rsa4096Idx) + if err != nil { + return nil, err } - RSA4096Keys = []string{ - `-----BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDg2CKBOThkgFX7 -wnaJsarejOnBjFgC/FuqhxIbogkHc0qgYOJxJsijFp+SDyL8iAt4FhyiOCXQU7cn -G3OpeIuiiXdgWz3QAkXILJX9h2iEzoyyFXWLOVvod3c/gsm/Up2g6NCnSXbADK4c -DJhWIZKETD7ZvI9cf+kT8f7QGi6j6AiIv+7iwVnyyx1zzOrhd7+2rHJF1BvHGc5s -fJ1zbyGx8oPjddmf0JjqA8sGmfC3+ksDviv2HXUrVpg51GosI4Q6euoyHWj3l6N7 -uwVC74WVhz3iIxEuMI4zKKeY1EQNM49My1kbaTt5farDMtR9gwjdacihchd+w3R3 -lKXeebNpukgzBEhM7CiBxJoPl6YLt0HEzPIu1PSxrysQtvTZQwrfIhhYNlc9S26l -P4ggbtlq65j4O/AztambvB7KLlYYbyL3ksD9PpZpco8ZgmQdZVT6GbelaVFZC2U7 -L+xJ6d6Aot3TALuH71j3EXP8Lbhtlfm4pHkFxbp23UlPUmANelUQBCumODWSjJ+H -5bFWnW0UUrVMWNqjaJEs8la1meocbrI3TmUQYgKEgi2rOSUuqVrpmZl0319x9qao -jUbweYXNabfYn38/O7OGeBdF15MdsYXgElERnq2D9ys8TIpGCWDZJuE2Q1tSlVEt -95/9AYm1R9LyYGJ43nCrq72GrZEIjQIDAQABAoICAGekGRuZxZ3F7lxzQfKse8fE -CogV6gfOTW6ofjdQlHrjsPWGUzq3FureJcXMxmLDTw4WmzJHUV7bB+S16bWnhC3y -0Z0P0clNEt93ddf8j7gQAZig/aKkWPIqB0S2Q8q2CUS/rFZALcXO0n7Ja3rgVMo4 -3wu8uBR6PXTdKojAWNlivnRSjInneE/LQpM7VNWDPlK04KPBZuB6y8UEGAu6oTyG -Pjcd/qeFHsdX4kDzupsDe4qJIXh8EaNdciPN/vbDlFLkj4l0NwtR0aDaD03QXLIV -OfhAE85HwXRhfAedaJTxPE+4uCVDd4/D1m/NwVbtjKuK9Fk+3wemjswEMAUB8imf -qJc4hQLTwF48u4qZJIVV2XUBBfp6ztgbSJ39EYlFT75KcLcm29Dr4Yz9jSRZ1WGe -BkZ5BVhm9QCKJyUgmSNKbgntxjboVR0QhdJeD0iSrbeGTr0qU5YQiuOG2PUm1C3n -H0oMpcsjE07jCLNlQqMLrXYPeAZkzwpr+5SzJTTY8CEyIzr6qvwe8U0ghz+MXAsC -NlvOVLKbg+fuOL4/JcGIAp/RyL0msSo0x6BtHABBfnxfNKqmJJ4SOKLglWMaHG7X -fNP2EUSTPFlVH9G71nzmZfmb2CuVsYBuG/4FVIwiWfljWjE00owR5f7sOcDUdPxy -AKbqwNMl3jDRlShdjqqBAoIBAQD8MZGSKyb1IPHjbgBmljHZrRzQjVLHyL3VX4X2 -sdjxMf5BMOANOa7LzTTZ86VUJL05KtNZN/rx3Wd8Wittsp45UBrOiSeLbuoV3PsH -/v/NMh+HeHeuC5fXoqB+gIs7DSTWzgoBEprITtu7T0omdqPKzY6cOY7JK+jzc34h -kD4J9KlpxmkpFyv4Q1fNxvf75S2PsjK+C+8B8qzjlb3IGFkvVH/XSIFAgfzjhoEs -OJ2gCCpc/9nnXiw60igfQrQiOJCWbmsV8VDRUlmGHeaT2UfeBVYri5qkDlfofuFb -DB+SdYJ+0L1pmEdhhLlV4OBeLcMABvKeT7O1lKv09N3mH/PhAoIBAQDkPOSjNBgA -V1Sm4Rn06Q05oVfs2AM0WqWJ6QWSV/Jjpsx4iw1arGSLfZozGyE55LlUKcCW/q6S -ah5OzdrP34TKMoRl6jisyjeys85uUEfJwljHXIeTUXrUnqFbKCu7X7h2AxSqi52Y -z8UiEFYmfjLsT6pMvIeUpC/xFtWq2/8UinL5KxqWI9fOImApO9lvcVkjHbE0OvSz -tsRO+7U4lk9LtA3h97r5JRFLKmdoGG0pGHZCOgQ6x7i9X/iGjk01iRvgwVow/v+W -s2fC4782o8U52w+gmyYip1LFbST1luqrtpaVdVY90erEdnoWX15p8FuKLwWaGAIT -UJlwjI9d+GotAoIBAGqjiyqDlLWSeLXyjbjTScEBsm19VMOl3p+bBMqL6XAT868d -O6BcA82pt+9xVzd9UYYa1cOkKDidpmSuvC5mmQEjHjK3TEFpZRJZnsyCxye9She+ -mNy/ijVkTvku4bDWnf02ooRRmaGZttA9dt1MzXWz9dmZfQcTyi0naO5IT/NlwT04 -6L63TaBs2XPp7nJVNi7M6yfxY98u/mw9pUI3CX85+9TMk9rzHDwZZAWO6xgAW+l3 -RmzPJWS5+L0/N2xA/uKdTiq5H7NjXveXLSjXd8wp9YX0Qi+c4Q8ul5woRDFp/wQg -v/cmrQhPVw85R6aLMymPxoeqrBLcCtpJsIreeAECggEASDfgXKfCJHF9rqQxc2Y+ -sgqUaLPdJ7a7BI1AHxNG9jM0JbxSCGveEKk4no0qEUiHP9NKRjzl/hwn5OWMJBRk -zxn2/MyFDF+cCiaM3ij23idpsgNcPsgcZqSfB9oJJGvgUS8eXex5fH3ZsbKbn+h0 -soNuroNFH0pohQ+loj+kUdqRELiL3BARW/9SkBmI7pNeEhd2F8HD5g2hxiAtMm+V -Pa4GaobZmbYZ57/OIokAGW3NFZ2H8xV5Jir015a1ZYgx0wc7Q5+cPhIcdfVcbqyZ -XnorUrVk9rgdH60ucatEK/tFYJtVI5CFiY63iNa6aCkOvgWs7xpDpdruAkfnoWNW -LQKCAQEA6ghY9gAjf8pcEWP9V2mByzOxsuYlZpRNPDbVhQ2iXIdm/xPjnvBmtsrV -9vIVmpS1wIua2Nvq23hJRdKXkws8jNHDuxezJyXVLoE78LNySMM4BO+Qs5Q/61um -D1+mGveQ+UUnqjpL/a+EebdlcUa5iGKWS8MpadFpXvrWWVvRKJ9zaErc0EB9+n4+ -dxykulk+PlxWFHCT0mVtxJuFXYTVQoxcXkEIuODBJvMBoWDtDdia/KtJh6QNOeQJ -+cp+Pupl6JJV1zbLPYNfoN8HkhDbYG99vsDDJ2oSVNBovudvDag6JPTH6prEO50W -cjIJEVjODcQOtyvsWXYeNL6vxiS+Bw== ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCYWZ7WnnWWEpnz -sgVwdxV074AmcFWFkYdfdvby/ajpdkDLzs3JVTsBdrccgsTtpis4c0vkMC9EjFM9 -vi/M45KlSXSiH27O6v1gxJvTcKq5CkURf5KjCDY4xtBIV4mmE0FS68F7DgQx8BEt -CfQCJstCa+fNfSN9wF3C7cQKNZZc5AnR6DTu33Q2T64n0tTtqP2NQ1+ug6I6F7We -WyhsMTdHDGWZsEZ63Y5+msFc325Rt9wU92RZSzRDxWazwgbtbeF5qysD0jhUdF97 -WrTfz59U8DQ8el7seAMKiM2ezYtCEpr7NU9Xj/7vSU5DR9FWwWAsKOlQDphzrJlr -8m46fnOoMZKZaXoalMXYtnqWJlSHEY7lZLdtur8Hn1BRbzxzvpwE+htH34ekf4i+ -wNUDyNtF9rDq+1ez+X9HqX2wKnatLfT4Xl8QES8oEDLNJ24g5bKITjwnvuZ706ck -rS1UnzFu5A7yRwpNieJ8giqxTHccXihGwARq4MA2BhX+u4hNtWTvzx7/7QFVe2UX -UF/5z2p7EbNf1FGFKTyptygUzOzHLAkuUA0t3JFkk7ZQlh4NzCJShXHMoLgwwzsB -CJ2g6BhzeGO/PTWPCWIcC66y1CmjK2A7LM83RWYHQ0dx5zAiRVrvWajiTC8USmY/ -5L7PaRPhu7vtqaAjLuN/cFWcUMYZOwIDAQABAoICAAQw+wC2jU+apWeh7ypf7FQy -PplQH9oz3cCPmk2nEt2RZkj1II44bQ4mQPVk15weJTRBX8YgWTyOi7+4GsKNRDyN -38qSCM9igaQG5K6Ve+zpTmsj0lnv4OYA+jvzRJMwFmz8lW8YPJ9PFnnhuzfP72md -ZFwv5CkSgMHbHriVLtTRZT/EHqivtxfDa2wnDbqYGpmnT9uSPYVRn6qCoYMQe4/V -1Hm88sNwkvoCse0nLHbNEx30jGs0eXSf2qp5XQ1tJ3XccquBPhF8vh+7qBw8Lwr8 -LMO5LiFdT+AbUWTcDMe9kUv19H/ZM09L+f453JWBbm7olTqhnCoahoIEwmmTjwT3 -sgVYS6aS9ayHRY44+KVIZt17dBiD7s1lQ2HXmBXkPgHt+WlBnElEfAKLenIyoinZ -pRbcG7qoMmcDumPTrJin7//jcidrQh4IZanX9Xm0qvkdrXCGFKZhqf0n4Y4ps138 -J73QO3YDnp9AFdO0p1pNI/60pwKBepRJnXVTpWeyDXunwX+fQZZ1tKu762VQsfUj -ZqiMnzQ2SnNJodrF1KjZs7rwuBFNwnuN21UEH65rk+ZniCq4ONt/vlT0/bV7UV9e -fYcwhDKhLYvApTJsz2THN2dF8Ajbr3H10MQCb5DT5f0biLEmAOB/LCJWBr/Q8+L7 -HC1U3D/EqVvrmCCZ55/RAoIBAQDCaa5jdCw9nnPYIubhu0tKN0AWnkeg+iczK0eZ -SFh/eAvOzpzEsK6oz+wMpKZfc9URSe5pgE462xHwxqlJV414gj44OFeRq9zBDMHj -G/x2cS/rI94Qqp43bdO0aoDDLuA+nIUqlWsYrXEKYjDGEMJBQkO3Zgp59VjdAWMM -X8IiaSTtJQisuQ6q62W2CxcQrEFzRlF7zHBO8RkfSE45uH7U8N13kPOVbrQCFiXN -ecBNNqYNhgqy+xF7g3GpldDlCS8H3vd0Y5VRHWf2dliN3fgQJ7J+7TbhH16CUvIf -xbP5vwAYcROT246y8cQhy4R5e+02Kpb64C5pKJugZhMd1J/5AoIBAQDInMXHRCOj -7tCAVg14udqnX+W4pbgOyoJrLq2ww3p48WbtNBJ3nwkKFNNYLv2zwNLAWkc53mNX -aNRxYXkG1GR++X8FEAWuAgvEwXvqQbg4ouuIpGYlVciNQ0fnMiNVwiPw4VJHlMva -5WjLQn9SJA0WLt9Nqf82yguP6gVOgRiAN1NwXbpfxflj+taTyCUKGULwYWuXK6d0 -HQ2ru16vrpPTP/sFxw0CTYZZ12rlggHH2xBdbjThuuPYSEkvOpOFtQJLMOmSGcyo -MvcSq+s9qQLope9tqWDW9RFcLyn9tux0YpJSbT06YaY9xcTcfuMLdwYg4KVWWjSJ -XjuQ4yfK1ffTAoIBAQCALwQPiQqeejoxeRm4HjDPN0ynXScnSajNAS1NMLlBGprW -eb49pa0TmzwtPeaAqzQCGTf8EeACyGy0z5fQxsx9d4qksOl0H9gG5W11W/+C7LBG -nriNTqHRNDXRECPkDaVHQxY+dJRPihQtX35/KY+bTaCubLZdoVo9JevzvbOX0rHJ -NpDYqY+1PE4s0HUdxiXFZsjVEn++XRNOX/NL9YySxFmRu14P2hUQByBXsX4Mqwqj -ggzN4+KsCIfJ5AD+8qYyz11jPUG3YOcqRu+uYntr03DKZYe4uWEsFpqUPlujQL9E -XlIlPC+DZwsFqVWocACApWY09dlD9sVd5c+W7JmpAoIBAE856rnJZTHmrJ0iXuug -qoOTUcvcVpYwz9S4eIvoh0OgQn/HIsvvGMjMdLLMzBDjQ9gHTz3BYAYzNkMYtY1G -7/FdYxaCv1t7H16y8tcO4Uwsu0wZcOWpvSxct9bMbDwAEeWddrsiPmfSVdKADnxI -FIsAM9weGNZwcKSDTVk1jpGESAWGXoZYTyd29qsiTc5xL4hzUORArz8iAjYiJ0DG -d0bka6Raef7A0yBD/Mlq0H03PH1JesTy4+yUj6KBRaV2WNONG5FBcxuyFPVgw+R0 -DNwIPEhYrg/2dMatMa4rQbKtAxBeZ+HYkqiupBJYYh4zAU6WqXUy5rChcHDF5P8s -BVUCggEAH3Qm6zmqMy24EVA/2K8jYYW0PSY7u8ABh35RZMBPdbUfTueBdkmqip2C -ja6JruI6hVU3pgSjUFLNnV8LrieskUBqeOta7eBurGCLDaHHyjsh9ikgE6GGvEXw -6YSE3xVxB72viwtiudz7WqCbsFO1cf2j/lzMWfC79CfTjCMuAT96p+f6rbVmWkSp -7v+O1A4d6JS5Fl6LrKC4JbnCbtbFGjP84eYmE3fjoWbFsZFHoKo7WTbVZWp80tsx -XMJdcL8JcV/F+SpFYf7vuCVWM0aB0LqrA17pWvugoY7iBn+qvbPG3sewwdFD93oz -pGE13QkqPIkPVUNcrqfxCjZE5wAK5g== ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDKSUJt0EQMIbwY -Vzqn5yeqXEHmtRZHdJU9jSOz7mnp22ojlaIlzID00Jx/4nXpSANhmicn1IDAaNw7 -lkVgELyxrKogujSDNU4aU1lkGma6tEDkD5mOC5J8Mp1orlnLjTzR061vUIU0dbuV -lv6OjCeKvvnDCjCnujF7kFDZZoZUeKIjB97VKXiaifq02/lrrcNwcjcsfG1sZZFw -kmTxDlDBfN4dGXY2FG8K9Rfm8XK3luexmHDcx+OwDFzv4F05pkb2peVyfg+qPx8t -j4lch5YMYLc6chDINh51DokYa/TzfiyA728iFkayzXZZlIEsPoqN03K2Cb5WaktQ -duarGBY0xmQ7ZVH1+5Q+F6PMjHEMSFWTbiwgNpBfuHsNE0JJHtaeY7T09q6xhRoH -59HOluwi6eOkXR4PWcmxYVxwSe6WY+gpTvEblPGRHYuMD20msAu5cQTCprTjQAra -WMsHLGLmvhQAmMywnfybgmyGJATIDFPsNmj5ckq2tyEZGki1285/FQrHV55gsWTC -74HBohYCLQQnwqEgyQ6gMRWdWHKLyP8xwoEi+KBEYJuAsJFd6fz+5w3pjcBXBbm2 -/vCNv16fyZoULF6FSEyY2FS0LcSC3Hp/n+Fw/b8Fw5sC9utX37ZCVUpPkjCUQjzv -rSq6eZXnCmJltAP/5aRWH7/Pdd2fHwIDAQABAoICAB/tgDak8JiZmn6dBf0KVxBk -j2JCosmUdRnJ9SCpOL5Yi2Aidf1RUelSI+FrdQDlBOOa2SNIPyofYuNkzH5lJeNF -RXT07uTmvPUawrkyEZTWboeQjsQEv5Iqyv2Cx1mBaWAU8QLoyp2FF558vqDxLiyQ -CAvox9UxZi4CkUA/FmSuxaiRzXIHoamCrbduIOgF/Rr6bArxeLPrNBF4icYiZEyl -0Mj3A9l1UDGCjcs9wMWJY/h7/xRZ2G4pBWI3H3/B5uF3PcfcbcyJOfqO9TdNjzlU -6pam+k8fe58uNCVOpNLpz0xqfjf7HB2MoGlzLxA0rtnDzg89anVpQYnpum621ool -2DiBfZUC1FjSA+fsIK+QxptunMZfI9XrZmdvmYxXeBdEWr8BieehtowY8+kEDLHA -VB7szebNsDTVShfxeZTqU6RsqGiUnm4/DKzewsfLHdfg76Oda8eraUxxpdlsqXZ7 -SOW4mGrH/T6CJsW0MmBUkI1vah/7+aUnUyajNXWu2Qvm5z66il4v21kNrjcGGWkg -zEOnAZQhg7CkV1LTkd66ZzsJBZXrVmOuwCkmA8s5RwBW/bjXE5vUkr5LgKlyCDzq -KWo/Xhibkq7Rfe7w4dOmbaRwvsC/78jF7Vdtaho0ZsdMpjfaYi0V30r9X1tAp/G/ -VewoWYWKh9ncpP+uAVSBAoIBAQDSf+jL2R58Wwk/zHWshgDVDPetqgnX8r8dJB0v -5WRmtWjpM7SSnln1GXJavvLiOCehJJh/Iwjofc0DjtHs6iVPgYwEwSzspWhScONy -zJlUJcncsTdoylC06B4KKdHB/ZiCkMwcxecLbH+Zam9+eq5rLdMpANSXV3EeA5is -3dQgDaNhgLE+R2tOy+uwjUJFUpu9ZpbtWGdDC/Ao82xh05pJGYqLcFwRlmmcXDwC -u36ofE7G3tod+MFX1juvmtDvMxYswpTVcNBnuPz9pzEnnqBLGwAfYj9bKc1p0/3I -bTNLnKTYtPGdzO2vdbQ+YFGtoYSmNuh0AaGpVc1ZWJTrLTwXAoIBAQD2AtqTYbbR -jahPtKSu//CvhH63ZUFvMTO7To0kFLpTC6jL1CA16OnJdtaA/wK18pm/ylmnagH/ -eii8EpZa/+2EpZsav4rxxqTMBZMk2jPrf7MGYUpfyZSLHJn7auIysHaz58I1MUWA -8PaBVEHuyyBjKAc78fWVN7+JEhGYFO6xr/YI/iqlSnebYcb8QDX/5UaBTRJvY2An -YEh7j+XalpIpOnS4u6r2krB3umuHsv7PAodjvwCqq5ilA67Um4FZiGuoKyNBVtbj -3GjeH72oeuiAUGE/35PewiVmftHVjsiM8re8AF4jQzGPgIdYXB0qHkxuq+XUwa2H -aMIUoxzE33I5AoIBACa0IgepKjPPQUobu9823FxQABJMW3b7SSyAgWVXFjjUTi/i -s+bperzYKvCIf3wcuxyj7+4gcPjeeJ2Y2vxmqOmPdkFBi5MPbrkJzKhE+kRAlncf -loKMAH7i1vMjcU/r4ujO2tjHgo3VKzj4Gvv0brGCQXsejfgtuby9CItwVhp2p327 -/drnotFgKTvTHUZFxCD1Bfcp9AKd5VCCQYFTOOEL8y9kP2l0cIKxas9NziIIiSuD -ujMck+AkoeDN5HC2wfME6/y6u3b2yn2RCjawseRdWI5ssB2A+CXnNphti6rxaFX4 -HxzWmzVRvQxjBWGZexxcqCz8R88s2Y79/JOpQ40CggEAebdVv3+j/TASK0VcCX11 -7tEmBMTjSAlW6ABoUoay3S2ymQ5d1W5kZRoX7QC+rZjXOw35p5wKWwVsrAiiPWnM -cUmiYOyN6St4E71aXOxcrdKjl6+BJb1Ncjp3cO4j9iJayI4NB1ZWZgJSZBB1Apmb -b5O5aI5BDE/lwwKek9kfc+h0WWSvYtJiNQ1+bwWx8ebVKFoimdvYEgNQOVorxiej -LyTN5Sxs3Mwc5U/lreEAsxk0NUSmJsr5ngMhd+1sZQjbAvw82DIH5fsCI4wewQH2 -kK20P+71cUwrRvfOB9Q6I4pfH3QrH1U6ax6TWENS5qjQ9hy0fLxKh+lrxNwi+sD2 -+QKCAQB0kItTBGLpUqlbuv65klZHYbaiGY+9Nd02lyb0oorjjesfmNk7Gu7HElBz -HOAyLtHOUqDmGSvdIwnSWPTBw6PkVi5avpniootHJJA0dCdvf+CjduVNgME+0lRm -gjKcs153/ZFW8cpxGTAEYHlscWHkOMMxw3NV1Bn0iTR4CgmRC34JE0wrlbPKjDlO -51eTngEgef4iKqzeqlu4mm4JCCFgmWo65kVP3oVMcitEZURsQA8qwUMHwwMzCenZ -9I3/dDGRiCuQXUUpAsCVDkyUF/pgnfm0Uiz75Aac7yTYnI5Ls6OTOQGjLZlRoBh+ -J1YBRcVoKSfYSY2wfkhkMB4t4WwO ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDILondeoXmtDyl -5B6laY+c6rB1iL6hSBBhlRchgR6JvjjpgYCyu0zyM9rK9SUuEnEPeskpGDC2/F8H -FDPNQZt/iXVTDn0lzElt8Y5al6Jn4UBzejMS2jd7GpTqrLbsmvwoHkqaEhKpRlNL -+zPP00XuUSwaAz9STpPC8XPwaQ/8/zO547r0++QegmpMWJX+MMTWr4iVaUY3pMqY -nU+pEppgYsgL9P8OJqRKYZZGJfx/mhW5vZxqj8473wYbfjSb6Yil4G/X2CtrtbnD -ajLjo1ezmyBhiyRusOi+bMTrvfH8aUDLIajOiFMm7Va2MMo/qnVvIBuokUvodQ43 -k1Y0yK6UtblwVtrMryekbu1VD+uuAxDf8+2P1FT5uT72g8sdjHlrUH7shdL0mvpi -CRl5gjAkx0kchZnBSbc/g6EDolEf15xaZpRhruBq5js5ynVsw1BvqQERpx0AGIxo -wVAd5rpVysmQtjhH9pqzf17+lcXU2w6XLNGrijPuIjtk+nN8Ln7pR2WbTufiXToP -qEVhBVO0W3V7M3mI/6AkzrORKiIKLqJZQVR3bGJTk2Bz30awY0WtKsfBp0/0bctH -Q4+pMunJC2yHCjDi4bLIfB9vc/+9BtI6DJVKHHqSIwcqc+vj3sDQRJ+n48U2awm9 -KU6Lw+OchmLWZCQbLLlZOsah8f78sQIDAQABAoICAQCWhNfJHx2b0R5hJlwcyHjh -5l30onOkVclrdm1EFhBeRhgJDAU3XzqGS5NSVG42bEty9kJXkgbfMHhLnFcvGQvR -JAyB/T0AtNDpyF3fpHDLZ0XNDq9kl4xsFhqvMlsBIbBEgjJaEwjRGz36vdHBjPzi -K0rb7GEqhEb6qA8jT/xjoYjFV4T2PIjUn/9JlLqDvSvkuWwb3GKd6F0lz/NcD87g -dqYwx3EYtNmHzETwThPD5po7ZWU9gw/xWMsA1S69pSXIhC/Uvog3nxck5q+JFwMU -26nHbpZgXMlElgijsdlIj+PWWWLSmRLdDEzNBXat1IAkpKHnaAkJqyqbgz+oxNTv -SlqP5NoFOBL+P1S2b2RQG6tpuBgon4oKGwkZLka/MD9l9QYbIRYcKMmHwxV9/SOh -sKhYoKh3AjIGFGla+Qz4dEWIwZ6zdTBsxjP7KR/Jt/Vz9noTOBm9JFNx2jsn0rL7 -LeMcOglq2UW0ollVvXvvd8bV3ku4EXnVpkinC8SOeB9W16DE+RjwvbiaJyy/sOLS -TOd3D8to9K7xsQVKDfOjGDGJsol+u2wb/TyuKozoTLfw/gVc5h1YPX+gwvGCSGtg -AJPRoymxtSJbk2BNg70H/fIfzlCfh4GNgqXA5wap8+QVplYyIDKeEp+iVQOLMM00 -iHjGjoYX+B4SLWTwQkD8AQKCAQEAzHymrBdhK9I+H7yPJrk/M0QBZ27YYCEc0H8f -2xsBnuaW3iUzE6DZJXt88KrgqfrsiGkVWlDQhlQSfKwQj9H9F2vNtx/RBu2aMksw -4ml3qoMdpbd9QIIw8t1jCP451z63IuFU0UbZl0/u9Rxw6v5wwNH1Gq2JOec7kq1V -4AAkP1xivU5Kvqm737aECrPgyV4jne0MaAYH8qGEKVDNu4O9rK5b4SUm7NUamkvL -R90i4JVo0IHYA/Ub4E4i6cHJ7TbtTcp0fShOW3KdLYATym/7Fm9Y3x0SkCETW0io -oyy0L2XqLSeKRq1w0GfXWUpMgGZHtQc+LNFCCriU2qjwcNpFkQKCAQEA+pxABd++ -Kkzgzl6ywExGhO6j3lPX90T3fd8J5TskEZJscLVibGGaoNgxwqKP+oSLFXXTJr/L -NdFfAuhcJ6lSZJoAxxB2PsKrA9VDUuc8Eo/adIHlLrDkVQcroomCJ+0sTHaGXTWl -wgaRa7v13/eV30rpLKNLtjSeMhhSmEP5XvQREziB+qhuSthWzdtGqjnaWEM0RYLG -B5pMpzhAkC4Ey+fywbhiMA2a1w9s0lQfxd6GYaiXpzABhs8I7bRzpxYSEgrqyyYK -skMzCedIm1hb/KgBelqF6MC+///nf6T85/Bh8EyxfjBlFabj7bjrF5mbJkzF55ug -tr8ZviPV76U1IQKCAQAUSMUrywHrm+ZntYepurSHPFa7UOaL2p0GHaYmUO5/ObZ7 -gMspRkpkCnThVsIEeoyeF3ZzyBJ3UL2oulTGP3lQqnP0l2ZfvpAOLyFBRF8Hfgwh -1SrKjF+Yp9dcHAPW0zTNc/a678FD3j0A+XpGBUlgBzO+GrrDEKn7KdCb8MentV1i -E/McKLAnR+6fNSq2Lu1vjAUwCHEfY2A9zPMrh6z4BS++DLZoxdbmuWAH1+rOxmNo -U4j/E4BZZsbV01BZhJpTniKiC66CKcNnsQ3FhggtOIxjTXn67B0EcBeyYAvbq8to -AUUZL7lCIxrck581GXBBh99mCLf5Ykf5zMpVF4HRAoIBAGBOZWgceHbG/mkwCR9O -8JarInwQ2mCitz0+1g2qcYzzKQsTGVSvGX4QNucmE5BhGRXRJqiwccYnxIxYgPmY -3xnb+MqG7/nkU1XwwaN9Sx+S+o9lT45m2gg27jTTBRqU0T49Ght9v0pVvdKZ873y -5jxeDEdkJXdKtzRnFm5/SLiNsHYjdAfAbEoE4y7OwlQuUVMz2EWSIMnRKP3l5yHB -HYTCiQ6a7dirkcJtohMd0uv2PMwQvt632w5UR4kZnIwsNhuK6HnTD687lcSLheJ0 -zTzFz3OWj/lHAN9eFzd9TtdPEEQJJPhqXp44eUTkmCuEkxPf0vnTW6p+u8TO/qrO -YSECggEACgemHnMpPTOmbb0d+OujaIsio3foADEfMt5j+SV4mSs00oIOuw/uouCK -H+eOX/qxfZPJ3pyFsh69qMLevqi+bipC9eb/mAM4zF85guhmgaLKp6/txP9JODB4 -j1EhnQMaSPo3/uPlIgm8pKDCYIgnq1vgoZ/ABqOQqzvkk3SEjbhej9oMSuaezN/o -iv3vHFgFCefQ/uUzd6hD3MYYfTJjxtojI3o2dc/Kd09zB7T+j0HMzX5B1cdhE9p3 -spWlJlc1zL2ENrODzdFfoHJTHTmBx3/NoY2MPswWdZQ7k4vBhRyr3VvwJg42Umz3 -8et6S/QprlpEjtxvp12wZN3Rw+FyxQ== ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDvfzJh+dbhV0Tx -tEaW12JhDYSEeDRl2Z/UFPyA2XitOreZ2+qhSp1OPu9AdsC4M+eGvoi8xqfQahdq -+34WXy6Od7RfE00kbgMXwlquxjGm7dS40qpYd1oGgMiKu4qf/V2DKAqaSFTUwPrk -pxYcuuV32vD5a5JGBnhLlZ042gOt+mcQW4s/FfHcda5DvKowvCAl/I0F63SXP4W3 -bt7v3OKuWWUPYg5z4Yw3z70GBxFQuTVYPbCVLbeKplGt7FkTx/1v1TfwHdTf2l7e -zfHBN/bJ/sRdjiI6/iY8Tpxv5KU9PjtIA4vfwC21DnZtnjILBUDsVQWmzqyJmV4I -YF40boEWTjXrLgNs4sYMYQ2BxhpjVjHeBTDSyI3Ch0RttkBTxW2Mn5+/+Uu8xg6/ -f6cRz6G2H/IaZS6xYOmczLVPJzTv3SGYkgZ4IOZlkSnCl4h4+Pg/nH01eCC53cOz -uuFjpmlGacnyjPd6wzUZZ36WAHSoGLB/lFAib8XIqakuMI7dzDEJVyUWHDF3oP9I -UH424xf6Ue7hKipUc/8Ebg6EX4zTvNN6zv1AjbNnd9eRpe+8CapvHJRWLrvMZOPH -sTG0/sgkpYsxOv2TjHBYeSMuxma5mjdbHNtupu8WtIwO8WC/ctClvGksPBAvcxNg -aITKB6WkDd2CKEXWni6Zuhh+TPxHAQIDAQABAoICAQDgb+rfNirQ8d+CQtcD9MQU -/334Rk2UROUq8p/Of/4/GI+GeDjg/fN8qC2904u3E60c8OFjRydsgH+Bmj0G6hvP -Fw7JKmVYhmAPm+svbjyJmseGjKVmUjLjdTx6BlZaqC1CA/wrqS0WU+LK9Goccmko -cNzyYISratTAwGyeInDgUZDUG3XaoM4PM5kjkWJMWYAnGQ1vRr/0HWtKA3SYg/9K -NCwoxlOU9QFaLCuPwn/PjkEXeHhK1JT0MfjJbHvttbUjYEUTmGEtho7xbT3pPHDh -ywZqRhB+CD38tg6ULUlMo00ap3glLwumaO9CusVa3omA8AxjzbqTGE6uuuCNciE0 -3e7IYQg2JKuw51ugzQJqJ+7SJ1gl0wFUaD0Eu9RNKuc0ZOay2nt9nh1bkD8blNBl -3PGxgrKayPAvk6YFgAiIGnDkO8vw1OPWpT9ebTYcdGKi5vjy8qG59SNWW4UAFPmL -SvqOFHkBo2V/F1u7Kogsf3iTWwhUPtxXyDk8h0AJ9czmAPCVFvt1Iaxo/WQrbPxl -eTTeH6u4g2zlTiC5kCRBYTWnAkj91mVtL0gqOtb0TKtZOQ+bwjmPs6iR0IfZLXH3 -IYEM2SCUsVRnyUwOy0wVwxaV+gcH/j6F2fJPztVBEhANT5BSDD9ExPepaRx9Igc5 -/P23M42p+50/RFG0CB8cIQKCAQEA9r98LmgqHc0cADP38U+/I6hmSbu4S4ZRVE// -no0nKc5PWnFbNmPdb+aPBeXCh/31vuUacogvZ1W+sUFpqjhJnB0UgQA3lmsLm8ox -aM/DheYazlG/pOzIm//pOqbUZK1KuH36z8vUdDQyQV79vaDMCl3C6O2wa9UeEEGX -O2247VPaIbMZza40UC5oOyDQbbOpIPIe7h2y1s3CvrhEPZ/K9P3mMLfzFGY2OJJr -V0no1srBvNMg6QhZW+0PDtseDHfB147vk41iTNMEbnzpnaBP4unRDMDsww6UGbsx -eltDEmPgczv0+1emZutbscMtUXXHyQSrxc/YG4/2XepzqL8xMwKCAQEA+Hob1Vqs -H6Iujntmpe0xE3RAL1ORny3kmrB7mCVQjv+2mbwVU01pYj/TLrb01ALgothRqVaF -MdqX1JoKNe8cOJOtZX1yMLGoav6Djt+d9pe7dR5yebcTEyhqb07ix/rfkA80zNjb -xx7nftioEw/Je8c0PAaUQ/Zw5BijmAHkuJ7YHfmuHH2tTWh9eIPx+ZPb2wK7A0lB -FHMlXHq7k78Cji625gg2KtLuAFK/p5Pa2nDPM8R0uQYbYQfFkWQwFZTTs3DgI+XH -3bR/1b8DRQHmAFi9PzwDFdYFuH5TVuvPPsYTJrTUKWf/6Nt+yqw+lMF3nK68osyS -VE4HKMF24FDO+wKCAQEAluMJgSdZedfPY6Bj4kCt1ZRI2JXeYPDGExTIb6BJbpsm -k1v2NxBifOc0VprllluKRy26Oodk1X/tmF8zyk+ZU8nEnA4R7/2Nn7rI9Br4qYzI -n5oF40iYdCzN5nvWLap6os8G7MLsLBMvGCKKb8dAfqZPZjjTRV2RgMdbP5AdIaPB -JBJtmQUKIG4Adwfd7PeMWQU2PM4Uap5wlEgCEt0AM0h/1xLlpnfKeFWxJjOgGpjq -WBmTam0cl8YjCyaa/WzOMI7LmiM/FVOExjvEcAt0ToJEv9PJ9I2ZqxJ5cyUTos9I -la74ZCp5Kz6JV+7Oa53mido9YD11HYWvVkbkazBfWQKCAQB7gEXhT8YJmxTE4PTu -N+ySnM9iNolEswzTDjEAOFvIF7VsyB1ZYDqnCM4wg+NAlYWNqzM7lbNyShH5K+8z -S3uda1ld/nIJXeQ1+fbtxpu++z/DQLTpZmNmvEnatTzm5PzFn6lAv/DNEcFCPPGd -N9WPXj3KMAL1nMITvWNipF5InTsR+w3dP2Ip/WuPwRU+VY2LV9oYEgr03R6Ozrn4 -/5GHlhR2VVKHCnwdUQPNiSHYPQXf1x+k7zIgkjpSv5dewrBOmiXt8cHbomF/ngdD -/2OQfIrjqTJnYg9J2hAWPfKuYskWDf797aE23hIxLleUnGyVRgygZkm8+WN5kF9D -syaNAoIBAED9uiEtW+4pNErEfkQ+1gW3xGDMR5e7VbZxX2Ijg3Ih4OPU+vGLR6i7 -/V0ES7pBrd7QDpQoDTX0FlOa9B0DzTjn7ksSk4QiySBPzvDNfMEursQIXBivX3Ps -7yRrm7WlgFC5dSlVEx9Iy/dh1zUFA04jIi8wVTTmPvDjhgyXxKp3hPnFWrMYdmG+ -MmRVTmRyDFsKyISntY8uedMnWJBQ1wGUfrcyS+LC+eITQxqXjUZOr1iyJb5P6p/P -fDSD2f9wM9yJtRodalmSUc3MjpGjvP7bDq9jfLU7IFl9SF9Hv8Ty+jyDilXEukIy -AiXjA8gwTuFuQqV/ctSCwLkjwIsEcm8= ------END PRIVATE KEY----- -`, + ks.rsa4096Idx++ + return key, nil +} + +func (ks *Keys) NewEC256(tb testing.TB) *ecdsa.PrivateKey { + key, err := ks.NextEC256() + require.NoError(tb, err) + return key +} + +func (ks *Keys) MustEC256() *ecdsa.PrivateKey { + key, err := ks.NextEC256() + check(err) + return key +} + +func (ks *Keys) NextEC256() (*ecdsa.PrivateKey, error) { + ks.mtx.Lock() + defer ks.mtx.Unlock() + key, err := ec256Bucket.At(ks.ec256Idx) + if err != nil { + return nil, err } - EC256Keys = []string{ - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUK/9GrMeJQc8qBZE -usHs5xdZrX2sUHPzT0mlkmf0ltihRANCAAS6qfd5FtzLYW+p7NgjqqJuEAyewtzk -4ypsM7PfePnL+45U+mSSypopiiyXvumOlU3uIHpnVhH+dk26KXGHeh2i ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgipJGI9PTJH/73dNK -FsyCqqKnt+qtWs1DhbjQy6NHMMWhRANCAAS1gg9Toh06WJuFU13OrQ7RiDNYAxOt -e5uQ7WiQCfaI5YQmKrdxnorlULQqfIpNC+cMqg0W0DFGL2CJzwDTJ14Q ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbhW5pPwZVwD1FLhd -4NvL6ZtPsKx/i5t6ArO5lbfK6GShRANCAART9CBatWeRBczMayzJI9Qys16itcac -iCUzwOJgFgcApP4OH66qjqyf1u42FBCtkv6YnS2w1uXUuyIwS21O9qr6 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQghaGmv8BLMP5RZ54Q -+Wh/QJZRs3rnt5mVn4j/T0NIW2uhRANCAAQAaTwOsuc6Z/fjiW/4JUrRslRSO0s5 -bwMUmgyzoaoewA0SUSOmRT62xzkjuCIrkLy6suaTtyy3HSdSPg87x/1j ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRGCG38Hvc0k49ZoD -MBJDsLspn3XYUAtHW03a/yoCybyhRANCAAR2MFW2GPrM7G7wWDPg6cUHZlMLZwEB -c8rPuJbAhPJV6eOjddAxAqxIG75Bmui8izCT9VDwG/lamwoootAVrVET ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXjRHQbCEpWFXm+qv -/fi9fvOvKzxtYcqr3pTBURRwZ52hRANCAASsrmNAipSpVM6oPa6A2H71lW7rP1ee -9X4lzW5VZ0F7MngmT28Hz+w7HLkC1/B0O6/PBKGu9S0EjB4zXxX+54L8 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgn4G1H1zvIE6ikcP4 -iGip12phmDQr1mdMRCj4+EXKN2ChRANCAAQB5xXAAAofB2PureI1sBx2WN1/OP4b -AoDqcvBCDAC+VYXnOuccSJEIIy10t6ETm+u9lpBma1JBK9WSllOgx/fF ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgpcxZF8IWJ1YoU5uU -yG6joajuVZIvUvOCOwk53aCboUChRANCAAQTaxOAFC9xlHu8VFxsa7ZdN6QKhOc3 -MUr5FPxnH8QLx5ao/GrTNJyyuz1qqQSw1919PWYLsQ0luhYnJvaiLPgZ ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1gUmTIMwMxVYEgSw -/ompciPQr7c6HXVfuLMnsFPK3eyhRANCAATxYK/Ij+rFAoF6F7Op2omhVEaY/2uZ -W9weD1EOCgoFaK3ckvNHbJUeE8HKrQVlKfU8gU2+/P8n6KQaUyo4GSld ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfLHhXqfTxo/2VYFR -jU1VVLbM+mFwYpwRYOgUiZy9o6yhRANCAATxGm/9AV0KEV+0DSJHwqTf6fIsAfxx -Hc/rTqjzVpXtoWyWmLOz6G2y1Qj0ZW/3QKGycpiw3l1GrhAGaK1ul2Ax ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/4Px3iT0ZQA/BgVp -IWjCv/V0JA7FzqX+1RwiLqyRjYuhRANCAARS1HQR07JSdBH+hQpP2GD5dn9wKkUU -vHvxMYv1bnA6VhqgMCjSrWwCSfenLWEbRW0CAclMAo9K9SlFH7ZVvtLY ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgref4P6c+qBkyBs61 -dW7mcGTyE7ZBqPylWJB+C99+b9WhRANCAARF4VMkMWWkFPG1ctkMdD6Mp1fbHHyB -6TJXd9ltV6Eh4m8QKYI95ez0DFMx62bAS1W+J/jI5I45jadOeDDpq8H9 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgq/svE3eceSNWZPi+ -eS/ql0dSFBTZ4gsbFpyQRAb/5ROhRANCAAQEPUJGOZbiGwxcoERFF3Td2gzznfMr -13JJFFPcWbw9iVCqGBl152gAfCEuw4UXsfHMCNEma6+63P18DQ0odj2M ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgK5G2FM0u53z1N4nG -4mPb2akK1xNxEQgkkzb6M73s2nKhRANCAAS9+qa2cp44F+mg6Efvkk46dByas9G/ -nWHqKeCxknVVavV8J0KaxynYfZrKkm6tKcbyEXK5Z/T2Ev2jk5QgdbSI ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQguw36Cim6Dk5Nqt4O -cVY4/CSoSC/3lrvPO3h93FBHycGhRANCAASGRPrMyFWObIn6vrtI928aGfy8AvDh -MOYGLRJWaNyEEBaiSFRMVASQUfodsyy4Tu1VnnySJPJ+TatlS9H9PcO4 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3VG4V0mchdCfDTq0 -9hKoqP4fm6EYFyetRqI+2F4esH2hRANCAAT52Sux7TptEqqeldcNdcYLiwCR0GEv -iYyLQpt+8stBULfjoCHIXdKST03FPcQYZcOa+rllsxosQZgYRbbFlVLU ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd92sJBhlHwEY+UyR -+C5yWPPESLYyHM2mWGNOM6PCf0ahRANCAATa8/l75iR7MaHJQ3w6CIuYV9Df1Dn+ -0cL0VVsW4XeVWn1VH82Rfq2TZ5rSpDFX/yMutmhlDMhLdHzg/djm4k41 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvdEjjGU8j9d+65Kn -xHdW+LfcmpRzEDwE0GGZtV3al8ShRANCAARnyjhSsMqaXBML2l92JJmvD5RmmhLX -qwxdovUobYqeRFEuvfmce3ZQBxVi1QHv48qYcZ82Mwpgz6QbVWm/ut8g ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg6D4PRWoHGemv7FTS -PhX5J2p15zH1pRrnbk3G6xgLbTmhRANCAATab/8LzPBnCHbWFbZuv2qgg9oyZAAy -glsqgXknaTksIYPN7dWuTKPVxnyEf+9r7cN0tnnQSJmcb1pw2rhD7ldd ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5zuq9eLti1n8DE3i -6HaGLR0poor1778p1bdT5Hy7uvShRANCAASWj6MkvYZ5rNDMMEhT7luavjurfP8B -0hSxtG4pCeFBGyzPq7wn2Kv1UMFvYqKn76Gvjmzt+6caGTXkz5VY7w8X ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJt0OTLJCFBkRKxwZ -fNL+ZfcoAljJ74GKt8NXbiC+Wr6hRANCAATYb2BkJCJFXJk/HWO/fOOjYYIvT/09 -PhLWbzZlU03nuate7dZUywpbXTMk4Tyr+qSNYiLODBelfcsqy6AIjIMS ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/+nEFDDOU+A9m1ba -bdXi0dI4qo+g1WDKqMpGpTO6h62hRANCAATDpW9e3GZwZUHQaO8+zmsKcZLs50JG -gfkNN9lIzfcz32VQUBdCItQMhOtMviqs0T9rd6uHfA5Z/VpzWlLjWdDD ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSP6k8VMAgI1KNoC0 -ObskgkhzSxqqvTiJt9S1RMS55TihRANCAAQp0yKgn1NLjerLbteuN1j7vahp70g+ -nTKHy4jnYCfjRht/f00CZmHjrh3zSpN8vnLenyLJCeuNu6LOXS8uXN+6 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQ9NPAUkjmdxMHfme -UppiDHbxRty8o/mJSiFnkyRD0gOhRANCAATDU7RtqlrUkCIG2id0ACiclWTmRVmF -01mMmTmE4ichdaEYhESbS2/vGCp6gh2bcrxWxi4PxBXEZyFiP0pAFFMi ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgB2ORqrDHa/sr9uHt -6zo1Ubvq1D3JRxiJ54irfL8yZ5ehRANCAAQXwPU7DXPEqwt+MwGuU/SzlWLjcx08 -k0yseAZNXxJchQ/iK3JVsgMMLDHEhby/s9Kc9sJCiNMMs5PFGOU99sdF ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgru9XNbWL7ESq4z4n -OqEmg9Wb1GpZwTfDnNj2qNAVw2ChRANCAASOPqa4AU1mzpXqFstiMXI+sHr792pV -uVZPNco126d1nf7tbDwFdhfSS34I2QbFk61ITOiSib2N/WLDdMREDQo9 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5NrGviGnBPJqTGPE -YrhZFOh06ZLcEyHBY8pP4DMc12ehRANCAAQiUNAD5PcYy+SJjpdmK3osO3EHziGa -cAiuDbmLHSdogmiNAbg3OHo27xcUrWbfOuaUsP38if8IWqa50Q2IiWqL ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglRn1lVBAsVf73PAH -to31ZMOi5JpJDSycgLdp+51wzZahRANCAATqW5+8EANPdjPdPD5TYL8XWjK5ix9L -9KimwQ0SouNH0pJxtWpZrcrxV9q17mrDcDLvadayx+SJaq8fisO4GsF/ ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg6b5CHiU3iVDr+S53 -+0D913tHSRamyNPZARB+iFbHJAWhRANCAAS7jPAJDeUXtcqwyP0/on8MjsslpGsS -/tUcjPOKBXY4bPPk1FYZXJ50GsfOOlgFskZwH0uhBmLH9ekJvwG77/N+ ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcwexygaDJU7nk1zB -hc/C9tzqY7ikN+hpWD9cJgXDWLChRANCAATOGxJ4oN2gYZetZTnekNGfqiffgfki -QXFE7K8qvNzX0Fw16nogXbHjZY2Ua4R4lp5Z+00klwwidum951kpUvMy ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgBYitD0hPUIKi1fNR -/0Ueujdr/9cfTv1bwjDJHTgaJVehRANCAATN3ZjuPJ6F/Y1t1voVqqYQIOH9beeJ -5fMptWuv9kkgjbgoovESeaDFb4esDkWGyp+kjm1lbw/cont15RbsFdrV ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgP+L3PyocfFX3kzGG -AU31veP9SIfGpZ7UOzaYejIA3Q+hRANCAAQ6TvuvFSM8WDJW+5Pnf6LHGlRJHm1L -Fdu+dCzctnvGhvnn00cra31egQTAPebjpH1qsDqoNAPQbemFfWCN4j69 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdQmY8K6OK3ew5XMb -2aY/8RTQWj9NZAAqQpSwA77qMQuhRANCAARknOvk29CR2pxpCeN0pYgUaAQKdHCO -wlqlyp91QBdUGZlx8J6/jgrZ38D8U2Vw363zVDXhckLUuU2Lze6gp4Is ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgLjQX3OomTDhQcmfL -50Fe7E22p4yp5TxYRgdLHNT0IuyhRANCAARcCJbXggCNgDSpWfOqDW0qS6WA4Uaw -wjJPTT/MmRZkYYzFVkTTFRRv5IVlBe9ZPARt2CgyUzG7/8t/JfdDYlt8 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeKc1FmPMncEdOK/g -aODDxbOl4xviFe8uM6AbI2RxVYOhRANCAAR7t7rKyFdhcQe5ySXXQFqZZ6RxKr6z -8+j/jivPjr9JplGcVBElxMPd5ld1M5T+Dwt9UdpGzSvTOZgJAVm8tHuf ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrysG2TPf0iJ7OLSb -BfSmKqJAAoDNt6xFOwa6ucL7CjqhRANCAARIX80NR2aaBGj+mnhQrjdzWVn5gSZ1 -q7q+H8ItoqRJmhrUp5ZwZJhHztubgqJd+k3beEUpYpEo4uE7wxg6nwYD ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWzQq5yIO+v1kmS+X -EuDVPMT6mqPHt3ZlFeh4KFOUCIWhRANCAATWSaxwM0Hvmy8oZ4QjWIdaIRPV1ifq -/KfFYNin0YE6oreMgieOyJl3ovLhD64lGfJxwusuCnCWnTqrHg9Azm+F ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgATitXRcKWwSV1tqc -EGiO0cy//HERRqqEdIbFBJr0nzyhRANCAASbK5LN6dHT2wnboNntpYLYhhnS+aSQ -IXgxbtBWW61ZVrCMcTMItexrvlr4u0nZnSD9Ix6WT5ppkVEhqUSdRH6c ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5ox3kjgk5NKpttqN -eNoFne2JK2dm3GEKcMLkLYVPefihRANCAAS74JK6pDiXKYrhm34kqvCmIVi5zqUC -+QKf1EkUeGbBSl2cYusavvdvvF1DKG/CkamB2gcmN/gZmGaSp3KH0cdR ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaDBRFdJrpHDYRlA/ -PnBiydaRlVIAgBDd0S+dwkRzKZqhRANCAAQqXjZweTWYRyuz/bah7+3x9jBgLe+B -OXqYp54tkgrzdWgyML4gIoTcH31yBVuDBpp7fXXwIgFBs8fk+HqCMZ0m ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPAaQPXSih2cZjt+M -4nsxdSFMywRsb/8NCzEC5i4sXQGhRANCAAQwtbeB0teR1+BN9aThPKh7pbqKqwco -FW+O5gB38oRn2NgenGp+BPNqud/E9YkL6P5nlGSlDdU+/y1Szc50ElwP ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgn6WlKx//YVUvpARJ -HJBVD7jttEgPkARRFUAqdTTonbmhRANCAARKcEk0vUn+/2axPe7dokSTQdZL9u1J -jtH2FiRtbAwoq8NJ7x2IPMgkMxk/UwzP5MKgfnse+TCEdQdctx2tIZzB ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgo6/01yc8yPHCGhnw -XwsLTJT6okgHKh9sLI+b3uY8+bOhRANCAAR6LcuoYD2zNInnuq3A4kw+cFAGHL2w -x+n/qC6YePKHgtKRNDtpPGOLgdtplauAJ3HGILjfMa8+3+nJJGFEhr11 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgo1cHdeg0VOKb5sUY -2yfk6eSHqseEfLjwVcWTKzqL1F6hRANCAATyqTu3OiC/S14IJECsrIBstUj3wsTd -dZgPp+qMhtGdU9BL0QFsD1h7ETtuGAq7UvzPF9FmWD64lS+Ya36PeAHY ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyo8VeaX5wXsXUIxI -KberdTk7GNSLi1LIgm+OPiWOP5yhRANCAATTvXLQuC687vWccDD/P1taqk5NBaUV -kZh1hM2WPdQ0KS9M79t3tMMhMr668hvV0NCgmTREf8hWXKOs5TNZft9n ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/sTG52OLHMyWnoh1 -di/+39XBR7gdZpSaGSe/5UHfycOhRANCAATkLT80tsAsHPa8MsVNwEx+SJf8XTjD -lLl4GbfwBtTK9eCVGMion2yWOfC7/d+3mmuxgbOjclKRhBtQWe6GWSj4 ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdWqZyrDmBRJkQnFG -UW7lM+W13VNUfustS0z2xdYUurChRANCAARVpDnQXx5xh7zxdAUKPtYgDw04mC+9 -xhxkX8O/pTUW1g5wDLo4ExixYeLOknTEdbf7Y+SS3MFGvZFP6CXJOslj ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcam2HwlwKA8f8oem -XwbD5BtF0kPSDHtNvGFSTbCUzaWhRANCAAQkrNVWSv8nhtqoE8BVTV7JOaRhGPXD -tm+460V9R+GiVb7dcoOViFtzjWmaFRjl0479XPRNrGb/ShccUFd6gTj8 ------END PRIVATE KEY----- -`, + ks.ec256Idx++ + return key, nil +} + +func (ks *Keys) NewEC384(tb testing.TB) *ecdsa.PrivateKey { + key, err := ks.NextEC384() + require.NoError(tb, err) + return key +} + +func (ks *Keys) MustEC384() *ecdsa.PrivateKey { + key, err := ks.NextEC384() + check(err) + return key +} + +func (ks *Keys) NextEC384() (*ecdsa.PrivateKey, error) { + ks.mtx.Lock() + defer ks.mtx.Unlock() + key, err := ec384Bucket.At(ks.ec384Idx) + if err != nil { + return nil, err } - EC384Keys = []string{ - `-----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBjTxA8CHSBA4WbaDC7 -urRFMmQVFFPelV+2t/qb1iqvURqZG91V6b8RflZPaM0Bi9ShZANiAAQZVLZ2lTkK -efb4LPehcttZ/QlFKau0XJ7sjRnf2UH5ISB1dFF2wpAIdOTVg8QGD1zlsUxXtxU1 -O/D5aGLYO+FzzUSWoDYvUTecBJ7M/fIXU5Jjv1nO7aP2N3rlnoTllew= ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCmlwFJbdQaLhlYtNj+ -Q/z5S4DKyr3BHk3P/C+mXxQGhdk7kcNa3z7UpXTQTi6oDKyhZANiAAQHczP3+rbR -Fk1KdrqjwHXmU1c/UAB1yqIRdVY5emGNPhDliSxvj/v0m3L5fbQ4eQSPGfxQzC1h -ik0u1WgJtwcCk9Uy2ER5GoWZWJM2KD5xhuvxYg5BMhceAJhyrrnpNAY= ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAzFIimBkLyJ8mixa9N -St59PStY8O3F2aesl+7aHYp9lj+Nr4ZuaIbtgnDNqQPCJbOhZANiAARInD8y38Aa -evDUJwq7mKtQUnPZ0zw3+knLI4X6WFuRHnTkzY01EvS+jX4EQkseC8sv1poNfpG7 -e4A1kbORUin/ubWQUnyabMWTAtx90Qsqz9Zw461s3T3TGY7IRqzsHZw= ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBiXWqxKYtBXQHJC7no -7J/cfv2r1O64LdtggnTUJUtXGyzrRl9xfCuQ2lVsBoCvRWShZANiAASMc8YnMWpp -8LaGqZnalZDdgBwx1OpyGbCtGaIsAi28S/iOOikIZy/ReJ11//GZLGlzgpcpF0PN -mNWWXaMtqWLPC2lzhrYIaE4wwMmE5CaFO0278ZKiW/etRYxTPKof3v4= ------END PRIVATE KEY----- -`, - `-----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCLgnysr20RBiL1dB8Y -1ZFxosfiUaC/f9J/LinX6s/e9IoURly4vCNpT1LEz5JfX1ShZANiAARxHhVrsVn9 -KxjijeYDejB4QbQrpEQNAGmfTKmwIpJyAZQgHgkrzzNwmuvXRhm3wVKPeONEg6bG -cD5liJh+nQFFpE6gZq3U2Kd+k1V98tc1fzzLOBHGGlkC2d69vUS0ykw= ------END PRIVATE KEY----- -`, + ks.ec384Idx++ + return key, nil +} + +type rsa2048 struct{} + +func (rsa2048) Path() string { return "rsa2048.pem" } + +func (rsa2048) GenerateKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, 2048) +} + +type rsa4096 struct{} + +func (rsa4096) Path() string { return "rsa4096.pem" } + +func (rsa4096) GenerateKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, 4096) +} + +type ec256 struct{} + +func (ec256) Path() string { return "ec256.pem" } + +func (ec256) GenerateKey() (*ecdsa.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +} + +type ec384 struct{} + +func (ec384) Path() string { return "ec384.pem" } + +func (ec384) GenerateKey() (*ecdsa.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) +} + +func check(err error) { + if err != nil { + panic(err) } -) +} diff --git a/test/testkey/new.go b/test/testkey/new.go deleted file mode 100644 index 80c4b9fc6f..0000000000 --- a/test/testkey/new.go +++ /dev/null @@ -1,165 +0,0 @@ -package testkey - -import ( - "crypto/ecdsa" - "crypto/rsa" - "fmt" - "sync" - "testing" - - "github.com/spiffe/spire/pkg/common/pemutil" - "github.com/stretchr/testify/require" -) - -var keys Keys - -func NewRSA2048(tb testing.TB) *rsa.PrivateKey { - return keys.NewRSA2048(tb) -} - -func MustRSA2048() *rsa.PrivateKey { - return keys.MustRSA2048() -} - -func NewRSA4096(tb testing.TB) *rsa.PrivateKey { - return keys.NewRSA4096(tb) -} - -func MustRSA4096() *rsa.PrivateKey { - return keys.MustRSA4096() -} - -func NewEC256(tb testing.TB) *ecdsa.PrivateKey { - return keys.NewEC256(tb) -} - -func MustEC256() *ecdsa.PrivateKey { - return keys.MustEC256() -} - -func NewEC384(tb testing.TB) *ecdsa.PrivateKey { - return keys.NewEC384(tb) -} - -func MustEC384() *ecdsa.PrivateKey { - return keys.MustEC384() -} - -type Keys struct { - mtx sync.Mutex - - rsa2048Idx int - rsa4096Idx int - ec256Idx int - ec384Idx int -} - -func (ks *Keys) NewRSA2048(tb testing.TB) *rsa.PrivateKey { - key, err := ks.NextRSA2048() - require.NoError(tb, err) - return key -} - -func (ks *Keys) MustRSA2048() *rsa.PrivateKey { - key, err := ks.NextRSA2048() - check(err) - return key -} - -func (ks *Keys) NextRSA2048() (*rsa.PrivateKey, error) { - ks.mtx.Lock() - defer ks.mtx.Unlock() - if ks.rsa2048Idx >= len(RSA2048Keys) { - return nil, fmt.Errorf("exhausted %d pregenerated RSA-2048 test keys in test; use generate.sh to increase amount or refactor test to use less keys", len(RSA2048Keys)) - } - key, err := pemutil.ParseRSAPrivateKey([]byte(RSA2048Keys[ks.rsa2048Idx])) - if err != nil { - return nil, err - } - ks.rsa2048Idx++ - return key, nil -} - -func (ks *Keys) NewRSA4096(tb testing.TB) *rsa.PrivateKey { - key, err := ks.NextRSA4096() - require.NoError(tb, err) - return key -} - -func (ks *Keys) MustRSA4096() *rsa.PrivateKey { - key, err := ks.NextRSA4096() - check(err) - return key -} - -func (ks *Keys) NextRSA4096() (*rsa.PrivateKey, error) { - ks.mtx.Lock() - defer ks.mtx.Unlock() - if ks.rsa4096Idx >= len(RSA4096Keys) { - return nil, fmt.Errorf("exhausted %d pregenerated RSA-4096 test keys in test; use generate.sh to increase amount or refactor test to use less keys", len(RSA4096Keys)) - } - key, err := pemutil.ParseRSAPrivateKey([]byte(RSA4096Keys[ks.rsa4096Idx])) - if err != nil { - return nil, err - } - ks.rsa4096Idx++ - return key, nil -} - -func (ks *Keys) NewEC256(tb testing.TB) *ecdsa.PrivateKey { - key, err := ks.NextEC256() - require.NoError(tb, err) - return key -} - -func (ks *Keys) MustEC256() *ecdsa.PrivateKey { - key, err := ks.NextEC256() - check(err) - return key -} - -func (ks *Keys) NextEC256() (*ecdsa.PrivateKey, error) { - ks.mtx.Lock() - defer ks.mtx.Unlock() - if ks.ec256Idx >= len(EC256Keys) { - return nil, fmt.Errorf("exhausted %d pregenerated EC-256 test keys in test; use generate.sh to increase amount or refactor test to use less keys", len(EC256Keys)) - } - key, err := pemutil.ParseECPrivateKey([]byte(EC256Keys[ks.ec256Idx])) - if err != nil { - return nil, err - } - ks.ec256Idx++ - return key, nil -} - -func (ks *Keys) NewEC384(tb testing.TB) *ecdsa.PrivateKey { - key, err := ks.NextEC384() - require.NoError(tb, err) - return key -} - -func (ks *Keys) MustEC384() *ecdsa.PrivateKey { - key, err := ks.NextEC384() - check(err) - return key -} - -func (ks *Keys) NextEC384() (*ecdsa.PrivateKey, error) { - ks.mtx.Lock() - defer ks.mtx.Unlock() - if ks.ec384Idx >= len(EC384Keys) { - return nil, fmt.Errorf("exhausted %d pregenerated EC-384 test keys in test; use generate.sh to increase amount or refactor test to use less keys", len(EC384Keys)) - } - key, err := pemutil.ParseECPrivateKey([]byte(EC384Keys[ks.ec384Idx])) - if err != nil { - return nil, err - } - ks.ec384Idx++ - return key, nil -} - -func check(err error) { - if err != nil { - panic(err) - } -} diff --git a/test/testkey/rsa2048.pem b/test/testkey/rsa2048.pem new file mode 100644 index 0000000000..edb6fc5033 --- /dev/null +++ b/test/testkey/rsa2048.pem @@ -0,0 +1,58 @@ +// THIS FILE IS GENERATED. DO NOT EDIT THIS FILE DIRECTLY. + +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPgKiyqylhbW96 +pRi4mM7PqouV6GheFvI6YW1X4zCPqDBDWLDKlG+KBd8wCtoofC8N5mXfzsDldsg3 +7aUlZR3DbiKO5rlPRQ89F33DQHZZE1LefNzsyxyJNYNyX7WLJXn1zI9KNMjzrPTu +pQVmtVB1pCSvxiyRAiSexos059LUudRzIUIFKHKNj6SmKSivgavsFTuNqnpHXObm +UMGmf9NLoY6VX1Kr9lpuch+PerVkrlAZaLMTZ5MqJTI6fnMFHbOGMxrbWUx5ZMXb +WEEBoIWW3O2ZcDMkUYPFR+UjtiJY/nTgIuQuoqzTKvl91QrfCCLjklYX0eYM4FUq +VgfXjNbLAgMBAAECggEBALIBoDatyLDwrYqb6MorJHdXyakPF8Fnk+LrQ1764eTL +FqQfiIIwtkLEaMOQ+7dxWPhmpwxJFIeEz5vS/TJIPTEy4OiQG3ZaOwlghp2iRiSC +BDwjB28HivJV+u56FoZI3wgytNWm1KDdxbyXyjti3aQd7O7xZbf8C6g9kJwRJ3ce +bA9kIcCWWb2LEedD8H+BPEJs867WlMnQjrIkG/xbyghfPbSyLMe5tLQGjSDxkggL +v67zj+D/PNMqPdhP3iVslsK58L4jkFabSotaXzCKzDxN7RkLBjKeRPeISZLghqur +Me+jeCpaoQU71ikp4LCuorPO+0mybDUhpmTGxaGWuXECgYEA4ZQI3GOAOtEaW++w +rH/WrC0piagUTDvv+KzbwpqDeUDP0gZ8ifL6wG2MCE1sq+Pq0ZC0sCgybLukiGoc +zxk+N0TV4D3HpW8b6mKb+DDWExNoZ2uQkzcWZoeWI+D+qCz7+G7vrrq7VVtu+f+l +h+9pGriOjim8Mf1s9iCk6XxSmoMCgYEA63ySPN/vLWVPc618xV82Oy8B9fjUDvqo +eUwsMME6ZAzO0m2/EIf1RAkSMxKmb9t83ecrCPfAekghWtdA/papKD9EMkvE7BoJ +ZSNRShKexq8H8fEFQzOhO2LDTbBDa7WQ1WUPBNBCA1MxiglUe05yDRRFjPpUkZ/k +PpoEfZTJQBkCgYBnWJPqrGdOCwihgCGYFgV64kH6gBe0iW06p68S7Ak53viXR0N9 +S+WXjVivYRFdetDU7A/r+K6JZDpQCRVjyDPZzF6UGpnB8DKA4maEgZNCMA0P/JbC +62UG2i1uCKGC2QEjY2fJzGERDQ+912K18XhctpsRBIvk9y8ZYAFNuxh5EwKBgHH5 +0S31lOX76wCqL4G4G595mRFcZgb5+yD6ZUkTvRc/u7rNs3Rk2akcWtqtZDEvorgk +cwfcIiUNVFeLZ8HRWf5I4NEXKzC7SWDSPz4C1SaFAOtxJILqMldz7eNkNL2lG3yt +dR93TPwfABM3gNRNm5YJAcDCSLxTDz3dfd7qbJ3BAoGAdkdfkqfR2zq/BB7umVfB +8ajMT2htjYahYmuIeO3qTjFJtvcKWhVwqEjwLcMFCPynV4FGZoik952NFNg4vhho +dPTAp1RavBVbGWDTVKkeafEKO75wP1C9E0Je5LKXaqdApnK4etOvyCkYxZhmoHHd +PWyNE1Jsl4DkO0Sf29k4IX4= +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNdZmv7U7hljYR +1O5XLG4HUk9wwSrerSfQ7Qotfz9Pmg/76NJxjVnlAXTn25/px97diAStgqcPdzTe +zzh7WsJjNZY9CJ/kqdYYsy/ti+fWaOSUlefZ8N15H9tOALjc0x55n/mfCA3/XXmF +dw1NL2bMO2wV8qvOajPcL7Qi4WGcOAjfw73USDsEdP2X2BIr9Bb4ZwY6WLkVAHA7 +CU2Fnbn1Py2Djh7HD+Jl9QlcdfCQxDCAafwrCWgrWpnJiUp5bnUUOW1eSgwHrCV8 +FHWa94wsOhIR+Fq368JnMkvdZQevTx+vuH8oBlBOytGFmBM8mCUkJlLVIzGDKrpi +ZTCEIOoRAgMBAAECggEAYJIqDrroHLhR6ce/z1Ge1eomVMU2tTuGP3lrEz+ALpYn +dSxV3fGmkzFAFcrxOx0q782DBVsn0ukg/KlBzxk1zRPe7gkjvoLlku6GVI0yB2F+ +LyjWtWW1c705g0xrl7/Tzy8WUV2j1qfE+qqeoezp0I0NnLNXdcoNXi096jctfhMw +0EEwog0cTlUZ8UyOz31DcAhE3qZPfVjoXn4E4BmFVQjhQRG+SvUvDOv/tzNX5l81 +vXQTNDld87xSPRjQ0mOdKzCBSXBb+lBrWQubPwIljcB0LapuiLfSBnwWqF6t5Y6/ +CP5GsVS4Sa8S1NTb1oJs4wLUYP9R6qveoxuVD0F3EQKBgQDf6STCAGRQHTAaVCTy +NGpw1jW67GVIsb4xs8Jc/vmXGEUgrOGqAMMFwPSrQT9J5HtM/nnme7QI/gs8NdBC +5Rpd9t5G9zc/RJeg3IwDjD1b3rtIRY8WTD2npR1hovkJhpPY5xSnwqQhg7cV7eRN +o+rqxckjZY8LslYuk2pZqx/JOwKBgQDq54MnBBPM493d/gC2OOzJbR+aCBUHgrxt +3uJi7cXEAFQNOKW/GU+atRDANh1LaILDSkZgaVH2hUiq2pp/Qj/Q6qW9+NPtN7l8 +iHivqtJ79p24j7BWgvamY/Rtdy2Lb/98LFs+Fc5XGKuUd48zGiue6kAi8Cm25wV3 +1ptum+rFIwKBgALesiHqb163gQ5VVcPk+BhKJpYmwYWVAaMRcsROYFSXcwtgK+RJ +7jX8qyYmx/DihNIP0PArVbtnxi0XY3v4A8aAi4jNUl/1ORxOt1y0R3UN/ciHW7Yl +dATaEO5XcGm2195H1/PugrwLPCWDzxFPsIshzdouSw8TUhd2vD45+0ZRAoGAJMg3 +myZiS1Tq6tXZGq9zNF8n8aCOWmy4QKQD4uXEb9p1TtSt72xxMJJlmxNeJu6oexfo +STR0pxtbs5UjWAXxpC754PNTi/OL0do0u50N9Gc7byjgvcsoAAnqvjFJKmpRIQp5 +BxG3C6BLTaYjACd66RlZDZ95iLBIBOnP0NQNQO0CgYAq8QXt3fPeQH9uBxDM4iZc +ouSmyIi8txm9fSOHGqCIJO05plLwsm86LKhSahGCRcMeb9x/tS6l5mEWYnfJ2VVU +5qJXqaX3cHocvNArHVMT+5WeLlaPxd/uxYFuzsoQBhwgdXvYpGhnbl+qHZaofr4z +MyRAxWOZrzW+DKiLPU0V9A== +-----END PRIVATE KEY----- diff --git a/test/testkey/rsa4096.pem b/test/testkey/rsa4096.pem new file mode 100644 index 0000000000..683b75433c --- /dev/null +++ b/test/testkey/rsa4096.pem @@ -0,0 +1,106 @@ +// THIS FILE IS GENERATED. DO NOT EDIT THIS FILE DIRECTLY. + +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDGun21EpRZIA2y +mksUQsnEuR8kQWpwqaenEj8A8hqGtBISQtXiy5TE3/YOOb5O+AggjvLZJYRV77I4 +rwQDWItMxrIxmqB7km/zzDVGbD91owTQnKcLBiaPfJ6GQc6i/pgSjTaFvsz2YQZA +49k9ipfAPP93ZlDvsWiCdyFaaYb6CNUm/6oSsVNb1p4QhTy40roZJuixR5owDQaR +g4tXemTgfxFkQS39afXguVNHj2f26AnCRn/wBr+sP6HhHMZkV+BDlhvPxUl0LKzj +/gtH4w9qFGDb8w+A2ys1Fna44dMsQDgqr+7HZ/OxtQxa2kpmQZwOM7NZwNwNG+iM +dEsnIXd4G0ODCe54WBMz86ZYMCpAQGx5ZTjYcUEP4RDUpyApPXIwfdf8M6Js1SYJ +QgOpUMpquvvpT72ksdmqYMc3o0/NGbXMIny2Hb9bIcxlaj3nScKTMZwydVGkUbOX +oKoo0ix7eJkYDHsMGM5ei1+/ryjNlhmDlizycVrx+AnFKHRmiU05U7lTiB8nnHnx +RixYbzz329GPIc3eHmUdvtcI8/1Vtp7cQVDN/1yApWmf3rAIvs/aoau65uhU/XbC +2JuA5IeuCCfjxBXB3snEsCY+CrJZ2WJT03/gMj9H/ui9GlZtwzd5gRwMpQB8cfpU +RthNimEjFIYzsgwfhazuLkgyyoef2QIDAQABAoICAD+oQ1Y6UlzOQLUCaaRe1IT4 +i7owXikinzqMRLRH2SlnCxbgY+UXM1txJj9eTdC78NaFE9NtChwBAQTZQx7TQSPh +zfjHwDp1KPleY35gdF95TbSJSZTlbnqt/5WgBNH/XbUrmNh0yvDtGXS1x8PH3l5M +68RSeQCewoxwHrX4ca0sISMx6Ee+l6YmdFF0bIQDtGsUJJuNBR35Pi5khcEKyr+C +1I5ZtqKjS4iltMCKdlIH+ABMVvULJGDHrVIPxpkj8QmVTulaF/Jn0SXjHbf0St6/ +ElvCWyf6jLefr33/kIZvN86stn8XlF9LUF7V59kjkRqXgw7wEUz9sJs1MVGijcLR +V+WKvc9eeSIjofAggsKE5R1+dv6eoI/DixE7sx1DnhDPeHEiEKN9ae1zMf8BaEpY +yNEujGpBfjfKOXb65xYo+AhPfLJeAx3/jCJZ71g6D9MtgOeJdgRfYgIUQmgqg1PJ +gWkpVaAc4OoKkhKnx7A54T6RrOMt76XfWFgP2e3frvzTgDBUkknkrJYx4HIr62Zh +c7umjtVsVsYjpEuvTPXJpUOQWQCc/Szx2DcY6BPZ4n+V9ZVVIMQuv56WuhE/J03Y +LlsmFsIPKxT73C/v59QRjlCkmZl0e3Q9p2jy8t+vwNyjfbP6zla2IEJhg5nkq/j8 +UF1vlDNro+zXBVivb1flAoIBAQDN/El3TMsEJy7sEukuntMpgYXWijalIHGmykMT +XhltKzvj0ZeIV1r2Gu/9DfvBqQFOeG5aYAa0NscrylHsbl6IJYW3wUezJCXm+vSJ +qJuwlIJueVBaN1D+23JbUNFSR/ErS2s/vHf0B0fHqBJVCY4fZl6Bj6m1YGPvh0i9 +UE9eKZGfplROGkzuj5nUXz6hRsKoVjKCU8/r2b+dgqq2cFbUzSuYc/WbGDdmNRjL +rRSpOGmbyQD6hriERQ7agVE+lG8vYdePTAjg1yYm+DUASQzc5dfacgT5eBRD1AFu +KrpVNYq3pz1Aj0ilJjh49+KJU5Blb0fhiTGu84nPaRTgaOUDAoIBAQD2+x7C6uqH +XVYHUaKCoy5EeAZ56K1QSiXdwfrqnZN+Vojt9PjZ2tMRmMnrAd68GbIXGd0h2zYL +rbmbI3EX76ZlAJuW5pPO9iFbU8DGats4gED2mKIavhBJDCZQ/SVYK0oQK3YPxKum +Chxw9sTaS3DbWQiuL9cCmXO3roDa3hKjCaZt4drjy8cxZB4sXq27aCmCIqIRO9bR +5nefuH5XZTQqNIx1cY7lMKQ66Sdk+ArT3vTs/plz7CMuI6BhKalMXMPPZMFvoLXo +XJaGVh8J2m4HXw5xShKCxpViI2VRkULwix3XY/a2qoXHQYlHAVaZbOoBbDy52KHe +Y+1HhEiGYWrzAoIBAFZ/6FX54JMo5TJrqpJSTfhzFMIIHnRvUGqrK0m5zVGjwy2j +OVAe5urMWxVYRu2HTC4osqavBoGtMyx3dLmli3r+zs1gk/xtZKE/p2sba+3WH4PG +2/BWpGOxwa8JHC3CWktFC4+jVHgcio8UTEZ7kbwr3+nma2zoQm82z1v4mqu/JxD0 +5xg5QS85DG87Y/CT53CLagCCs6CmOyoo0gl02XHZisIlh/EOVU1NZNE7KJ77OpYZ +7ZhG9LtOyLMHdReje6FZJA1f76aDktjwiElLY+RrfJ6WHPKp81CcedFKjh70MgF3 +cGhpAyefCj36Up46gjumZHgYhc2jJa78wLCQPAMCggEBAMyQzrfPb5XS/xBs01e0 +5PudFnAfAn5ADAETTErLXYEFF8FQaFW5Y51tmcDm+Z9/AP0VVQ1Xzfn6WINg5alX +u9BoonZoYQDI6HQGeONfWlgAEs6tOYdA6ag3Qf1Oz4GpyVx/Qvhog2uxcEE4g2/z +kHR92Cy+Py5N/4SiKuQdj/4uXgUhTvXisQf9zugdO3TAH7FEEkyH7bRJWceXPj4Q ++xYCFFyqRBsdIMoSl6iPshgu0VsCvgNAERuEMrCHm0w+gYjkATv+Nu1Q0vRNnMPC +gePlHcdD/PUImm6AtsjKslEeSQdAKva9YrTZWWTQfPPzPBcVmW6tOdVDmyLjNFbp +lXUCggEAT1TpUmmFjsfv22hBOkg0cxnOCVhMc9gkLz+6liGsSRg72trFu5HPI5df +QAj9kTXBUY1ttwabONc/qIW4rzw4L+B9aVHwlsJ/UAmYDcLs2POhyZyXW+TIumGQ +yOSp4xcChYKuAc5wPMo6EKW8fduy0ZDNsvG27D1TDMF5O5D0iDfNl5mxu8PK+arh +KHPebppQtib8Yg41FydtxB93TNV6oQ3uNr8rzHeMtu6wWsvOyRHE5Jo+FLVwQ2ZA +KRuh0NnvZfbkBpmtm/cAwq95kpDiXPTB1wTJEPkhP3raMdoeULcEkT/qZA+tnPmo +S0mC+5slUqAYDdmd/ThucLyeANpZTQ== +-----END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIJRQIBADANBgkqhkiG9w0BAQEFAASCCS8wggkrAgEAAoICAQDY5mOVDnggcAw4 +UIU7WY8i9sV6xBnTzLPBR6SxxaSGe6remY+6TcETJxIZdM3KiJHFpoRQrxfeqymi +4PGT7wM2KU0taQNjiRESvvRe4N+/7LxHuSo3En/yUSdQqQSAVo7XWWBtjpqtrxRv +iKTbdmjP7hw1FHGOY8fE4vuvGkWMlAOzAu/geFMyWVAeXnPUhVZrzc+RYSZelBD0 +MW5lKc35kFEdGXoLgkhXUk27MqwnkSrcJNkh6OMlMOpYWUT1vHR5K9CZ6W8Nl2J8 +QQ96XnMRc3C/KuhPEMymuiLj2s0bQ+p0j7apPHa2WtFATXOj+oaKO8dQIwP3TGdj +SWrVWx27mGGQ18YY+kYwkhvHaShIX6sVi/l0xc2R4DxVWKeKvpe/UbZrAt3dcOa7 +OnZNCretvk4h5Vegycx+3Ac0df1m6MLTZTn9+lOARVCpvFM6R5QLm0X/VJ3UvesP +PHXszPxxQkAeHuWU8rfORkji4J+7oBZjtLsV1IHKFAjq6iNJQ5pGYnvfP6DrINe3 +iKBsaswjp1ij+kbAUAu9QSl1ait/Np+79fLKlzhREqwjvN63pHFGsecOEx1QFD6A +Vqh13f5lGPYvjXr1ZF3EknVbsxQCpxjyqf0PuSqMBnJcrE7L3IKMmWtwQvFC9Sj6 +QTaO4hjLpZHNFv4Ne5oLJkenchJATQIDAQABAoICAQDTh0xSz7ujluK0AQMOMHeB +l7xbz+eIQTiFJIOfw6qCZRTs5kHfZXkIXrAuF1WjUbEoWw7rSPc0dySx7kJrDUvK +hFj6ElH1vnTiHUxhQ1my6QNtx00+TFJvVWnMJil3p/LCXi3Gaq669+YsJ8zvIvlw +3zyvH3LndLQcdWkTCcIOKUO6TwD1nyM0FRono+G+vxLbK+pkU6SB1FD8dUC+dBim +bHJOuMvncXVvg5q/F9oA9HgiHeWMRn1PhfbllpnENbg5e9uCXr+pN5wapbCcnIQH +3tdz+Dp68V1EtH7WTEp/bqq2Znmzbn4vtT4hQeenYenX4hitNJjnkqG5mJ4R2TyS +LTgoGWdV0YUZtmF3+GsDYZPlEyB3cyoFiyR9yEiXKGPe6A05urPkNTQNjNYlAdLU +Y5pEykYGO1mioCz6bBS1fZSEFasTj/4nrSNx6EOFLdExwn+fC9plbjNM4//HpXiB +X9K5TdeOW7o+guy68zb3ytNTZz0A6j5vKXR2FB62Xqn6Gi4RlcMRgGrbiQEylk41 +lmAh3jcf2JYexXfKZ8TinhKK9iWLe6drhBsFv0/ZtkIaICijteQ73nG9NS0RVpaB +IdgGqw2EMjbR/QJ2dMyag6iMzadeyAhDPInj4T6aPlM4/f/EOcJdqM41T6bn3a8+ +7yF345b5HvFI3futJhRPwQKCAQEA4jj6b4lrs1g5Tbt8rS9qfUEezStKBT6wjgNe +b6JGNXbZUdUax0qW53eGeC+xAn1BvOLVy349Hlt9ScMBUXzHuWPkX0mIerGRpmN9 +k/uEsDl+Rt37Z1excLeLkroNSlSFZsopwoA6dFGF1rgUc+HyKqHUEbZfy+Va9vO5 +BJZJoYp4ugE9YacItOwjipTQWUH6NfUk7BiHGm+WnfawcoXXH0r4O11irg65z3FV +bE/eqUZQflT6yKVSfEFcQqqr5rmBHWueRXUq3S2ugJFYkrj2GFa9QKTZp6jy+yi7 +DqO4FH2CkeT1ABtyrObSJ3IEAg4lAGmiCHuLGOTA6zDcCBB19QKCAQEA9XNEQCMs +d/OmCj7MKiXzvlIARBs2VEdbctmoHwsukPNuoaYjhmGfBPWXdsaMz9M35txeHXFX +77nItjI+yTcZyeP0KlXnAb6YkGHmsoniC60xW/63Nbm+feE1FInpT3+jyoaOa/MK +ASQMiurdJhY0qu5Ce5ygaFehyDntFwOanqv6Z9bow1ZzqKX9IE3wvol3IxGLkyfA +Isjo3j5QPHyHHXBNKrE2dqtDsjuTVo05rbp/P6G6mgIAQjHzBDmeiGy+x3mEv4jT +glx9M354S3chKQifFKdxpZbMF+6ejR24q9LtM6dRFAq+OKdrlXoj81O7Dhinv3z4 +lbHvKMxcrqlR+QKCAQEAvXbLCC8nrITvOVMVEFbt8QlhKqRe0hW2+LmJliVqd8ya +Jhc83jxyNlm8nVwT++m77N5uAIgx2AL345cWu5CuFW68DbIgQ+IEAj7BJfc5If6E +7AVuURb43VZb5v87sk0njPc0EloimtjMJxD00DkkAOCYJF2BzdrBXKKzCkx0Tn8S +rXXsWqTyfdRnz+Divl6rmBVAXxwLyvA6TQIWtVOy39qCG/YSd4SNylc5HAWojkz9 +jVDO2MzdUIPNKWiXoB0tLd68J6ABzkw8IiGY9QlD0w6SYlmukOTG2+M5BwHHYiHc +ASSorPZQDM8kozSydqYyBy5xLnmJ/cdYa6H4JijjIQKCAQEAzSMB9qyu/K2IpuVn +Ew7XEMhN6p3noTZmKq3YgeGBkKmzW6yT4jrygV2UsjMs+oCoJu0kR200NmnKYuPJ +b7f6eK5ooX1b7SxTK9B2097DKkkciKtwiZlsqJ4xE7JTaRrfVGNy4qukP+HWDcBP +BgbnC7jHnbIAqlQbJVGsYmCjuFs5k9Gcha1aSqg3zuj0/Pm8tXVzdpBxV2Ecpqnj +uznEXwk9pSGoyDNJB8wczuiHPTgyI4dSgmaLuscuOOjDI3fnVqWsGbwMMdaE2SWo ++kFdWIMZGVT8eY13k8TdhElDz28gydvbumlkI8tg8fO72iCvpA9dG4Ah7lJg9HMg +PuXKWQKCAQEAuBuXHigvjTOYEenZfeseghiVJrl2VdGpWkPzx9MU4rKqyWbYi4BO +BCmZIP2GiS6Qfb574B4xj5JGRXRipCyfqvUov2kXkwKWzxlAKhgWXvAoPlQ0McVE +vHMt69lFaZ/krjcGBsIHgMsHCtv7cEGehOKf+08Htr9vWuz6Ngwh2LWKVjulgSLj +mPncDDBlHSrwxHh5enFJgxu3SbdMKAfijPpc8gqXNv78WW8pKKII+o76NKSoJHB0 +TCFuEUNgYeEYVkV3yyPj4Ln48PPe5fdkgM7W6z12DpO1ItSWeNycpA4ryYpNonM0 +S2/D8RGzYwaP/msqFg+JYNtXDijdFtINbg== +-----END PRIVATE KEY----- diff --git a/test/tpmsimulator/simulator.go b/test/tpmsimulator/simulator.go index eeb5566421..ad2650d0eb 100644 --- a/test/tpmsimulator/simulator.go +++ b/test/tpmsimulator/simulator.go @@ -1,3 +1,6 @@ +//go:build !darwin +// +build !darwin + package tpmsimulator import ( diff --git a/test/util/race.go b/test/util/race.go index 088c7d0850..dd1504a169 100644 --- a/test/util/race.go +++ b/test/util/race.go @@ -3,13 +3,10 @@ package util import ( "fmt" "os" - "regexp" "strconv" "testing" ) -const flakyTestEnvKey = "SKIP_FLAKY_TESTS_UNDER_RACE_DETECTOR" - var ( raceTestNumThreads = 2 raceTestNumLoops = 2 @@ -46,18 +43,3 @@ func getEnvInt(name string, fallback int) int { } return fallback } - -func SkipFlakyTestUnderRaceDetectorWithFiledIssue(t *testing.T, issue string) { - t.Helper() - const issuePattern = "https://github.com/spiffe/spire/issues/[[:digit:]]{4,}" - issueRegexp := regexp.MustCompile(issuePattern) - if !issueRegexp.Match([]byte(issue)) { - msg := "Skip only allowed with associated issue. " - msg += "%q does not appear to be an issue. " - msg += "File an issue and specify it to skip a test under race detector." - t.Fatalf(fmt.Sprintf(msg, issue)) - } - if _, skip := os.LookupEnv(flakyTestEnvKey); skip { - t.Skipf("Skipping under race decector until %s is resolved.", issue) - } -}