diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6734c86e8..c5db92a0b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -14,7 +14,7 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go/.devcontainer/base.Dockerfile # [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1.22-bullseye, 1.21-bullseye, 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster -FROM mcr.microsoft.com/vscode/devcontainers/go:1.22-bullseye@sha256:44c273a0506ee6c7c13f4f0c1abe8dd077469ac9f3ae6be0617d6c59a1256089 +FROM mcr.microsoft.com/vscode/devcontainers/go:1.22-bullseye@sha256:46f85d17eff2b121269b4ed547eb366c2499b5f549d8eaa16fbe6e38f04dfb93 # [Choice] Node.js version: none, lts/*, 18, 16, 14 ARG NODE_VERSION="none" diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index ee56520d5..3f5195ca9 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -75,7 +75,7 @@ jobs: egress-policy: audit - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up Go 1.22 uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3f6b73d0c..5a92c3ad1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,13 +31,13 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag=3.0.2 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # tag=3.0.2 - name: setup go environment uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: "1.22" - name: Initialize CodeQL - uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # tag=v3.26.8 + uses: github/codeql-action/init@f779452ac5af1c261dce0346a8f964149f49322b # tag=v3.26.13 with: languages: go - name: Run tidy @@ -45,4 +45,4 @@ jobs: - name: Build CLI run: make build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # tag=v3.26.8 + uses: github/codeql-action/analyze@f779452ac5af1c261dce0346a8f964149f49322b # tag=v3.26.13 diff --git a/.github/workflows/e2e-aks.yml b/.github/workflows/e2e-aks.yml index cdf2a8304..0ce8950a1 100644 --- a/.github/workflows/e2e-aks.yml +++ b/.github/workflows/e2e-aks.yml @@ -33,7 +33,7 @@ jobs: egress-policy: audit - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up Go 1.22 uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: @@ -64,7 +64,7 @@ jobs: make e2e-aks KUBERNETES_VERSION=${{ inputs.k8s_version }} GATEKEEPER_VERSION=${{ inputs.gatekeeper_version }} TENANT_ID=${{ secrets.AZURE_TENANT_ID }} AZURE_SP_OBJECT_ID=${{ secrets.AZURE_SP_OBJECT_ID }} - name: Upload artifacts - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ always() }} with: name: e2e-logs-aks-${{ inputs.k8s_version }}-${{ inputs.gatekeeper_version }} diff --git a/.github/workflows/e2e-cli.yml b/.github/workflows/e2e-cli.yml index 67038445c..08a265249 100644 --- a/.github/workflows/e2e-cli.yml +++ b/.github/workflows/e2e-cli.yml @@ -19,7 +19,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - name: Check license header uses: apache/skywalking-eyes/header@cd7b195c51fd3d6ad52afceb760719ddc6b3ee91 with: @@ -39,7 +39,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - name: setup go environment uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: @@ -51,7 +51,7 @@ jobs: - name: Check build run: bin/ratify version - name: Upload coverage to codecov.io - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Run helm lint @@ -68,7 +68,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: setup go environment uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: @@ -84,7 +84,7 @@ jobs: make install ratify-config install-bats make test-e2e-cli GOCOVERDIR=${GITHUB_WORKSPACE}/test/e2e/.cover - name: Upload coverage to codecov.io - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} markdown-link-check: @@ -96,7 +96,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: submodules: recursive - name: Run link check diff --git a/.github/workflows/e2e-k8s.yml b/.github/workflows/e2e-k8s.yml index 717ff937c..be26f5362 100644 --- a/.github/workflows/e2e-k8s.yml +++ b/.github/workflows/e2e-k8s.yml @@ -31,7 +31,7 @@ jobs: egress-policy: audit - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up Go 1.22 uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: @@ -65,7 +65,7 @@ jobs: kubectl logs -n gatekeeper-system -l app=ratify --tail=-1 > logs-ratify-preinstall-${{ matrix.KUBERNETES_VERSION }}-${{ matrix.GATEKEEPER_VERSION }}-rego-policy.json kubectl logs -n gatekeeper-system -l app.kubernetes.io/name=ratify --tail=-1 > logs-ratify-${{ matrix.KUBERNETES_VERSION }}-${{ matrix.GATEKEEPER_VERSION }}-rego-policy.json - name: Upload artifacts - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ always() }} with: name: e2e-logs-${{ inputs.k8s_version }}-${{ inputs.gatekeeper_version }} diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index cb827af40..f6c1aba55 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,9 +22,9 @@ jobs: - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: "1.22" - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: golangci-lint - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 with: version: v1.59.1 args: --timeout=10m diff --git a/.github/workflows/high-availability.yml b/.github/workflows/high-availability.yml index 631f9bb6f..e9e576851 100644 --- a/.github/workflows/high-availability.yml +++ b/.github/workflows/high-availability.yml @@ -35,7 +35,7 @@ jobs: egress-policy: audit - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up Go 1.22 uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: @@ -60,7 +60,7 @@ jobs: kubectl logs -n gatekeeper-system -l app=ratify --tail=-1 > logs-ratify-preinstall-${{ matrix.DAPR_VERSION }}.json kubectl logs -n gatekeeper-system -l app.kubernetes.io/name=ratify --tail=-1 > logs-ratify-${{ matrix.DAPR_VERSION }}.json - name: Upload artifacts - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ always() }} with: name: e2e-logs-${{ matrix.DAPR_VERSION }} diff --git a/.github/workflows/pr-to-main.yml b/.github/workflows/pr-to-main.yml index 5fcefe211..fb7938dba 100644 --- a/.github/workflows/pr-to-main.yml +++ b/.github/workflows/pr-to-main.yml @@ -18,7 +18,7 @@ jobs: egress-policy: audit - name: git checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - name: Get current date id: date run: echo "::set-output name=date::$(date +'%Y-%m-%d')" diff --git a/.github/workflows/publish-charts.yml b/.github/workflows/publish-charts.yml index 850838750..45338d088 100644 --- a/.github/workflows/publish-charts.yml +++ b/.github/workflows/publish-charts.yml @@ -17,7 +17,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - name: Publish Helm charts uses: stefanprodan/helm-gh-pages@0ad2bb377311d61ac04ad9eb6f252fb68e207260 # v1.7.0 with: diff --git a/.github/workflows/publish-cosign-sample.yml b/.github/workflows/publish-cosign-sample.yml index 566c27885..36f3a897c 100644 --- a/.github/workflows/publish-cosign-sample.yml +++ b/.github/workflows/publish-cosign-sample.yml @@ -25,7 +25,7 @@ jobs: egress-policy: audit - name: Install cosign - uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0 + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: Get repo run: | diff --git a/.github/workflows/publish-dev-assets.yml b/.github/workflows/publish-dev-assets.yml index cf9083917..30b9b6c9e 100644 --- a/.github/workflows/publish-dev-assets.yml +++ b/.github/workflows/publish-dev-assets.yml @@ -21,11 +21,11 @@ jobs: with: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - name: Install Notation - uses: notaryproject/notation-action/setup@104aa999103172f827373af8ac14dde7aa6d28f1 # v1.1.0 + uses: notaryproject/notation-action/setup@03242349f62aeddc995e12c6fbcea3b87697873f # v1.2.0 - name: Install cosign - uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0 + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: Az CLI login uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0 with: @@ -118,7 +118,7 @@ jobs: helm push ratify-${{ steps.prepare.outputs.semversion }}.tgz oci://${{ steps.prepare.outputs.chartrepo }} helm push ratify-${{ steps.prepare.outputs.semversionrolling }}.tgz oci://${{ steps.prepare.outputs.chartrepo }} - name: Sign with Notation - uses: notaryproject/notation-action/sign@104aa999103172f827373af8ac14dde7aa6d28f1 # v1.1.0 + uses: notaryproject/notation-action/sign@03242349f62aeddc995e12c6fbcea3b87697873f # v1.2.0 with: plugin_name: azure-kv plugin_url: ${{ vars.AZURE_KV_PLUGIN_URL }} diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index e4d81f984..21bcf2815 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -20,7 +20,7 @@ jobs: with: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - name: prepare id: prepare run: | @@ -64,7 +64,7 @@ jobs: --attest type=sbom \ --attest type=provenance,mode=max \ --platform linux/amd64,linux/arm64,linux/arm/v7 \ - --build-arg LDFLAGS="-X github.com/ratify-project/ratify/internal/version.Version=$(TAG)" \ + --build-arg LDFLAGS="-X github.com/ratify-project/ratify/internal/version.Version=$TAG" \ --label org.opencontainers.image.revision=${{ github.sha }} \ -t ${{ steps.prepare.outputs.baseref }} \ --push . @@ -79,7 +79,7 @@ jobs: --build-arg build_licensechecker=true \ --build-arg build_schemavalidator=true \ --build-arg build_vulnerabilityreport=true \ - --build-arg LDFLAGS="-X github.com/ratify-project/ratify/internal/version.Version=$(TAG)" \ + --build-arg LDFLAGS="-X github.com/ratify-project/ratify/internal/version.Version=$TAG" \ --label org.opencontainers.image.revision=${{ github.sha }} \ -t ${{ steps.prepare.outputs.ref }} \ --push . diff --git a/.github/workflows/quick-start.yml b/.github/workflows/quick-start.yml index f6739f243..3d9b3aa6e 100644 --- a/.github/workflows/quick-start.yml +++ b/.github/workflows/quick-start.yml @@ -35,7 +35,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: setup go environment uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: @@ -59,7 +59,7 @@ jobs: kubectl logs -n gatekeeper-system -l app=ratify --tail=-1 > logs-ratify-preinstall-${{ matrix.KUBERNETES_VERSION }}-config-policy.json kubectl logs -n gatekeeper-system -l app.kubernetes.io/name=ratify --tail=-1 > logs-ratify-${{ matrix.KUBERNETES_VERSION }}-config-policy.json - name: Upload artifacts - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ always() }} with: name: e2e-logs-${{ matrix.KUBERNETES_VERSION }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e261c1bd6..b550b2372 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,12 +21,12 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag=3.0.2 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # tag=3.0.2 with: fetch-depth: 0 - name: Install Syft - uses: anchore/sbom-action/download-syft@61119d458adab75f756bc0b9e4bde25725f86a7a # v0.17.2 + uses: anchore/sbom-action/download-syft@1ca97d9028b51809cf6d3c934c3e160716e1b605 # v0.17.5 - name: Set up Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 @@ -49,7 +49,7 @@ jobs: $RUNNER_TEMP/sbom-tool generate -b . -bc . -pn ratify -pv $GITHUB_REF_NAME -ps Microsoft -nsb https://microsoft.com -V Verbose - name: Upload a Build Artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # tag=v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # tag=v4.4.3 with: name: SBOM SPDX files path: _manifest/spdx_2.2/** diff --git a/.github/workflows/run-full-validation.yml b/.github/workflows/run-full-validation.yml index bef82603d..1584a2d3f 100644 --- a/.github/workflows/run-full-validation.yml +++ b/.github/workflows/run-full-validation.yml @@ -63,7 +63,7 @@ jobs: egress-policy: audit - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up Go 1.22 uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: diff --git a/.github/workflows/scan-vulns.yaml b/.github/workflows/scan-vulns.yaml index aed9bba20..23208d9b1 100644 --- a/.github/workflows/scan-vulns.yaml +++ b/.github/workflows/scan-vulns.yaml @@ -31,12 +31,14 @@ jobs: with: go-version: "1.22" check-latest: true - - uses: golang/govulncheck-action@dd0578b371c987f96d1185abb54344b44352bd58 # v1.0.3 + - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 scan_vulnerabilities: name: "[Trivy] Scan for vulnerabilities" runs-on: ubuntu-22.04 timeout-minutes: 15 + env: + TRIVY_VERSION: 0.49.1 steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 @@ -44,7 +46,7 @@ jobs: egress-policy: audit - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - name: Download trivy run: | @@ -52,12 +54,20 @@ jobs: wget https://github.com/aquasecurity/trivy/releases/download/v${{ env.TRIVY_VERSION }}/trivy_${{ env.TRIVY_VERSION }}_Linux-64bit.tar.gz tar zxvf trivy_${{ env.TRIVY_VERSION }}_Linux-64bit.tar.gz echo "$(pwd)" >> $GITHUB_PATH - env: - TRIVY_VERSION: "0.46.0" + + - name: Download vulnerability database + uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0 + with: + max_attempts: 3 + retry_on: error + timeout_seconds: 30 + retry_wait_seconds: 5 + command: | + trivy image --download-db-only - name: Run trivy on git repository run: | - trivy fs --format table --ignore-unfixed --scanners vuln . + trivy fs --skip-db-update --format table --ignore-unfixed --scanners vuln . - name: Build docker images run: | @@ -66,10 +76,10 @@ jobs: - name: Run trivy on images for all severity run: | for img in "localbuild:test" "localbuildcrd:test"; do - trivy image --ignore-unfixed --vuln-type="os,library" "${img}" + trivy image --skip-db-update --ignore-unfixed --vuln-type="os,library" "${img}" done - - name: Run trivy on images and exit on HIGH severity + - name: Run trivy on images and exit on HIGH/CRITICAL severity run: | for img in "localbuild:test" "localbuildcrd:test"; do - trivy image --ignore-unfixed --exit-code 1 --severity HIGH --vuln-type="os,library" "${img}" - done + trivy image --skip-db-update --ignore-unfixed --exit-code 1 --severity HIGH,CRITICAL --vuln-type="os,library" "${img}" + done \ No newline at end of file diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 74be38718..a45432923 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -35,7 +35,7 @@ jobs: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag=3.0.2 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # tag=3.0.2 with: persist-credentials: false @@ -48,13 +48,13 @@ jobs: publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # tag=v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # tag=v4.4.3 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # tag=v3.26.8 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # tag=v3.26.13 with: sarif_file: results.sarif diff --git a/.github/workflows/sync-gh-pages.yml b/.github/workflows/sync-gh-pages.yml index 8c584c7e7..0990fc29b 100644 --- a/.github/workflows/sync-gh-pages.yml +++ b/.github/workflows/sync-gh-pages.yml @@ -21,7 +21,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - uses: everlytic/branch-merge@c4a244dc23143f824ae6c022a10732566cb8e973 with: github_token: ${{ github.token }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5acefcd1a..b634fac3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,15 @@ Welcome! We are very happy to accept community contributions to Ratify, whether those are [Pull Requests](#pull-requests), [Plugins](#plugins), [Feature Suggestions](#feature-suggestions) or [Bug Reports](#bug-reports)! Please note that by participating in this project, you agree to abide by the [Code of Conduct](./CODE_OF_CONDUCT.md), as well as the terms of the [CLA](#cla). +## Table of Contents +- [Getting Started](#getting-started) +- [Feature Areas](#feature-areas) +- [Feature Enhancements](#feature-enhancements) +- [Feature Suggestions](#feature-suggestions) +- [Bug Reports](#bug-reports) +- [Developing](#developing) +- [Pull Requests](#pull-requests) + ## Getting Started * If you don't already have it, you will need [go](https://golang.org/dl/) v1.16+ installed locally to build the project. @@ -12,7 +21,6 @@ Welcome! We are very happy to accept community contributions to Ratify, whether ## Feature Enhancements For non-trivial enhancements or bug fixes, please start by raising a document PR. You can refer to the example [here](https://github.com/ratify-project/ratify/blame/dev/docs/proposals/Release-Supply-Chain-Metadata.md). - Major user experience updates should be documented in [/doc/proposals](https://github.com/ratify-project/ratify/tree/dev/docs/proposals). Changes to technical implementation should be added to [/doc/design](https://github.com/ratify-project/ratify/tree/dev/docs/design). Consider adding the following section where applicable: @@ -45,6 +53,18 @@ If the PR contains a regression that could not pass the full validation, please 3. Follow the same process to get this PR gets merged into `dev`. 4. Work on the fix and follow the above PR process. +### Commit + +You should follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to write commit message. As the Ratify Project repositories enforces the [DCO (Developer Certificate of Origin)](https://github.com/apps/dco) on Pull Requests, contributors are required to sign off that they adhere to those requirements by adding a `Signed-off-by` line to the commit messages. Git has even provided a `-s` command line option to append that automatically to your commit messages, please use it when you commit your changes. + +The Ratify Project repositories require signed commits, please refer to [SSH commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#ssh-commit-signature-verification) on signing commits using SSH as it is easy to set up. You can find other methods to sign commits in the document [commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification). Git has provided a `-S` flag to create a signed commit. + +An example of `git commit` command: + +```shell +git commit -s -S -m +``` + ## Developing ### Components @@ -71,6 +91,7 @@ The Ratify project is composed of the following main components: ### Debugging Ratify with VS Code Ratify can run through cli command or run as a http server. Create a [launch.json](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations) file in the .vscode directory, then hit F5 to debug. Note the first debug session may take a few minutes to load, subsequent session will be much faster. +A demo of VS Code debugging experience is available from ratify community meeting [recording](https://youtu.be/o5ufkZRDiIg?si=mzSw5XHPxBJmgq8i&t=2793) min 46:33. Here is a sample json for cli. Note that for the following sample json to successfully work, you need to make sure that `verificationCerts` attribute of the verifier in your config file points to the notation verifier's certificate file. In order to do that, you can download the cert file with the following command: `curl -sSLO https://raw.githubusercontent.com/deislabs/ratify/main/test/testdata/notation.crt`, diff --git a/Makefile b/Makefile index 376a2d170..3d06df53b 100644 --- a/Makefile +++ b/Makefile @@ -160,7 +160,7 @@ test-e2e: generate-rotation-certs EXPIRING_CERT_DIR=.staging/rotation/expiring-certs CERT_DIR=.staging/rotation GATEKEEPER_VERSION=${GATEKEEPER_VERSION} bats -t ${BATS_PLUGIN_TESTS_FILE} .PHONY: test-e2e-cli -test-e2e-cli: e2e-dependencies e2e-create-local-registry e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup e2e-vulnerabilityreport-setup +test-e2e-cli: e2e-dependencies e2e-create-local-registry e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-trivy-setup e2e-schemavalidator-setup e2e-vulnerabilityreport-setup rm ${GOCOVERDIR} -rf mkdir ${GOCOVERDIR} -p RATIFY_DIR=${INSTALL_DIR} TEST_REGISTRY=${TEST_REGISTRY} ${GITHUB_WORKSPACE}/bin/bats -t ${BATS_CLI_TESTS_FILE} @@ -459,14 +459,37 @@ e2e-sbom-setup: NOTATION_EXPERIMENTAL=1 .staging/notation/notation sign -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} ${TEST_REGISTRY}/sbom@`${GITHUB_WORKSPACE}/bin/oras discover --distribution-spec v1.1-referrers-api -o json --artifact-type application/spdx+json ${TEST_REGISTRY}/sbom:v0 | jq -r ".manifests[0].digest"` NOTATION_EXPERIMENTAL=1 .staging/notation/notation sign -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} ${TEST_REGISTRY}/all@`${GITHUB_WORKSPACE}/bin/oras discover --distribution-spec v1.1-referrers-api -o json --artifact-type application/spdx+json ${TEST_REGISTRY}/all:v0 | jq -r ".manifests[0].digest"` +e2e-trivy-setup: + rm -rf .staging/trivy + mkdir -p .staging/trivy + + # Install Trivy + curl -L https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz --output .staging/trivy/trivy.tar.gz + tar -zxf .staging/trivy/trivy.tar.gz -C .staging/trivy + + # Download vulnerability database in retry mode + max_retries=3; \ + attempt=1; \ + wait_time=2; \ + while [ $$attempt -le $$max_retries ]; do \ + echo "Attempt $$attempt of $$max_retries..."; \ + if .staging/trivy/trivy image --download-db-only; then \ + break; \ + fi; \ + if [ $$attempt -eq $$max_retries ]; then \ + echo "Failed after $$max_retries attempts."; \ + exit 1; \ + fi; \ + echo "Failed. Retrying in $$wait_time seconds..."; \ + sleep $$wait_time; \ + wait_time=$$(( wait_time * 2 )); \ + attempt=$$(( attempt + 1 )); \ + done + e2e-schemavalidator-setup: rm -rf .staging/schemavalidator mkdir -p .staging/schemavalidator - # Install Trivy - curl -L https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz --output .staging/schemavalidator/trivy.tar.gz - tar -zxf .staging/schemavalidator/trivy.tar.gz -C .staging/schemavalidator - # Build/Push Images printf 'FROM ${ALPINE_IMAGE}\nCMD ["echo", "schemavalidator image"]' > .staging/schemavalidator/Dockerfile docker buildx create --use @@ -475,7 +498,7 @@ e2e-schemavalidator-setup: rm .staging/schemavalidator/schemavalidator.tar # Create/Attach Scan Results - .staging/schemavalidator/trivy image --format sarif --output .staging/schemavalidator/trivy-scan.sarif ${TEST_REGISTRY}/schemavalidator:v0 + .staging/trivy/trivy image --skip-db-update --format sarif --output .staging/schemavalidator/trivy-scan.sarif ${TEST_REGISTRY}/schemavalidator:v0 ${GITHUB_WORKSPACE}/bin/oras attach \ --artifact-type application/vnd.aquasecurity.trivy.report.sarif.v1 \ --distribution-spec v1.1-referrers-api \ @@ -491,10 +514,6 @@ e2e-vulnerabilityreport-setup: rm -rf .staging/vulnerabilityreport mkdir -p .staging/vulnerabilityreport - # Install Trivy - curl -L https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz --output .staging/vulnerabilityreport/trivy.tar.gz - tar -zxf .staging/vulnerabilityreport/trivy.tar.gz -C .staging/vulnerabilityreport - # Build/Push Image printf 'FROM ${ALPINE_IMAGE_VULNERABLE}\nCMD ["echo", "vulnerabilityreport image"]' > .staging/vulnerabilityreport/Dockerfile docker buildx create --use @@ -503,7 +522,7 @@ e2e-vulnerabilityreport-setup: rm .staging/vulnerabilityreport/vulnerabilityreport.tar # Create/Attach Scan Result - .staging/vulnerabilityreport/trivy image --format sarif --output .staging/vulnerabilityreport/trivy-sarif.json ${TEST_REGISTRY}/vulnerabilityreport:v0 + .staging/trivy/trivy image --skip-db-update --format sarif --output .staging/vulnerabilityreport/trivy-sarif.json ${TEST_REGISTRY}/vulnerabilityreport:v0 ${GITHUB_WORKSPACE}/bin/oras attach \ --artifact-type application/sarif+json \ --distribution-spec v1.1-referrers-api \ @@ -524,7 +543,7 @@ e2e-inlinecert-setup: .staging/notation/notation cert generate-test "alternate-cert" NOTATION_EXPERIMENTAL=1 .staging/notation/notation sign -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} --key "alternate-cert" ${TEST_REGISTRY}/notation@`${GITHUB_WORKSPACE}/bin/oras manifest fetch ${TEST_REGISTRY}/notation:signed-alternate --descriptor | jq .digest | xargs` -e2e-azure-setup: e2e-create-all-image e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-akv-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup +e2e-azure-setup: e2e-create-all-image e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-akv-setup e2e-licensechecker-setup e2e-sbom-setup e2e-trivy-setup e2e-schemavalidator-setup e2e-deploy-gatekeeper: e2e-helm-install ./.staging/helm/linux-amd64/helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts @@ -560,7 +579,7 @@ e2e-deploy-base-ratify: e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosi rm mount_config.json -e2e-deploy-ratify: e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup e2e-vulnerabilityreport-setup e2e-inlinecert-setup e2e-build-crd-image load-build-crd-image e2e-build-local-ratify-image load-local-ratify-image e2e-helm-deploy-ratify +e2e-deploy-ratify: e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-trivy-setup e2e-schemavalidator-setup e2e-vulnerabilityreport-setup e2e-inlinecert-setup e2e-build-crd-image load-build-crd-image e2e-build-local-ratify-image load-local-ratify-image e2e-helm-deploy-ratify e2e-build-local-ratify-base-image: docker build --progress=plain --no-cache \ diff --git a/RELEASES.md b/RELEASES.md index bbf03b1ac..1069f3f1f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -48,7 +48,8 @@ Applicable fixes, including security fixes, may be backported to supported relea When a minor release is required, the release commits should be merged with the `main` branch when ready. * Alpha and Beta releases will be cut from the main branch. -* For RC and stable releases, a new branch `release-X.Y` will be created from `main`. Required changes for the minor release should be PRed to the `dev` branch, the change will then be cherry picked to `release-X.Y` from `main`.S +* For RC and stable releases, a new branch `release-X.Y` will be created from `main`. +* Required changes for the minor release should be PRed to the `dev` branch, the change will then be cherry picked to `release-X.Y` from `main`. ### Major releases @@ -56,7 +57,10 @@ When a major release is required, the release commits should be merged with the ### Tag and Release -**X.Y.Z** refers to the version (git tag) of Ratify that is released. Prepare the release with a [PR](https://github.com/ratify-project/ratify/pull/1031/files) to update the chart value. When the `release-X.Y` branch is ready, a tag **X.Y.Z** should be pushed. e.g. `git tag v1.1.1` and `git push --tags`. This will trigger a [Goreleaser](https://goreleaser.com/) action that will build the binaries and creates a [GitHub release](https://help.github.com/articles/creating-releases/): +**X.Y.Z** refers to the version (git tag) of Ratify that is released. + +1. Prepare the release with a [PR](https://github.com/ratify-project/ratify/pull/1801/files) to update the chart value. +2. When the `release-X.Y` branch is ready, a tag **X.Y.Z** should be pushed. e.g. `git tag v1.1.1` and `git push --tags`. This will trigger a [Goreleaser](https://goreleaser.com/) action that will build the binaries and creates a [GitHub release](https://help.github.com/articles/creating-releases/): * The release will be marked as a draft to allow an final editing before publishing. * The release notes and other fields can edited after the action completes. The description can be in Markdown. @@ -78,7 +82,7 @@ For example, if Gatekeeper _supported_ versions are v3.13 and v3.14, and Kuberne ## Post Release Activity -After a successful release, please manually trigger [quick start action](.github/quick-start.yml) to validate the quick start test is passing. Validate in the run logs that the version of ratify matches the latest released version. +After a successful release, please prepare a [PR](https://github.com/ratify-project/ratify/pull/1805/files) to update the chart value in `dev` branch. After PR gets merged, manually trigger [quick start action](.github/quick-start.yml) to validate the quick start test is passing. Validate in the run logs that the version of ratify matches the latest released version. ### Weekly Dev Release diff --git a/ROADMAP.md b/ROADMAP.md index b98c8f72f..88d6fbf8b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ ## Overview -At Ratify, our mission is to safeguard the container supply chain by ratifying trustworthy and compliant artifacts. We achieve this through a robust and pluggable verification engine that includes built-in verifiers. These verifiers can be customized to validate supply chain metadata associated with artifacts, covering essential aspects such as signatures and attestations (including vulnerability reports, SBOM, provenance data, and VEX documents). As the landscape of supply chain security evolves, we actively develop new verifiers, which can be seamlessly integrated into our verification engine. Additionally, if you have a specific use case, you can create your own verifier following our comprehensive guidance. Each verifier will generate detailed verfication reports, which can be consumed by various policy controllers to enforce policies. +At Ratify, our mission is to safeguard the container supply chain by ratifying trustworthy and compliant artifacts. We achieve this through a robust and pluggable verification engine that includes built-in verifiers. These verifiers can be customized to validate supply chain metadata associated with artifacts, covering essential aspects such as signatures and attestations (including vulnerability reports, SBOM, provenance data, and VEX documents). As the landscape of supply chain security evolves, we actively develop new verifiers, which can be seamlessly integrated into our verification engine. Additionally, if you have a specific use case, you can create your own verifier following our comprehensive guidance. Each verifier will generate detailed verification reports, which can be consumed by various policy controllers to enforce policies. Ratify is designed to address several critical scenarios. It seamlessly integrates with OPA Gatekeeper, acting as the Kubernetes policy controller that shields your cluster from untrustworthy and non-compliant container images. As an external data provider for Gatekeeper, Ratify delivers artifact verification results that are in alignment with defined policies. Additionally, Ratify enhances security at the Kubernetes node level by extending its capabilities to container runtime through its plugin interface, which allows for detailed policy evaluations based on artifact verification outcomes. Lastly, incorporating Ratify into your CI/CD pipeline ensures the trustworthiness and compliance of container images prior to their usage. @@ -60,33 +60,37 @@ See details in [GitHub milestone v1.2.0](https://github.com/ratify-project/ratif ### v1.3 -**Status**: In progress +**Status**: Completed + +**Target date**: Sep 16, 2024 -**Target date**: Aug 30, 2024 +**Release link**: [v1.3.0 Release Notes](https://github.com/ratify-project/ratify/releases/tag/v1.3.0) **Major features** -- Error logs improvements -- Kubernetes multi-tenancy support (Verifying Common images across namespaces) -- Cosign keyless verification using OIDC settings -- Notary Project signature verification with Time-stamping support -- Signing Certificate/key rotation support +- Support of validating Notary Project signatures with timestamping +- Support of periodic retrieval of keys and certificates stored in a Key Management System +- Introducing trust policy configuration for Cosign keyless verification +- Error logs improvements for artifact verification See details in [GitHub milestone v1.3.0](https://github.com/ratify-project/ratify/issues?q=is%3Aopen+is%3Aissue+milestone%3Av1.3.0). ### v1.4 -**Status**: Tentative +**Status**: In process **Target date**: Nov 30, 2024 **Major features** -- Attestations support -- Ratify supports Azure Trusted Signing as a new KeyManagementProvider -- Use Ratify at container runtime (Preview) +- Enable revocation checking using CRL (Certificate Revocation List) for Notary Project signatures +- Add Trusted Signing as a Key Management Provider +- Support retaining multiple previous versions of certificates/keys in Key Management Provider +- Artifact filtering based on annotations -### v2.0 +See details in [GitHub milestone v1.4.0](https://github.com/ratify-project/ratify/issues?q=is%3Aopen+is%3Aissue+milestone%3Av1.4.0). + +### v2.x Status: Tentative @@ -94,5 +98,8 @@ Target date: TBD **Major features** -- Use Ratify in CI/CD pipelines (Preview) -- Support CEL as additional policy language \ No newline at end of file +- Attestations support +- Kubernetes multi-tenancy support - Verifying Common images across namespaces +- Use Ratify at container runtime +- Use Ratify in CI/CD pipelines +- Support CEL as additional policy language diff --git a/charts/ratify/README.md b/charts/ratify/README.md index af3600209..571bfa141 100644 --- a/charts/ratify/README.md +++ b/charts/ratify/README.md @@ -47,6 +47,7 @@ Values marked `# DEPRECATED` in the `values.yaml` as well as **DEPRECATED** in t | replicaCount | The number of Ratify replicas in deployment | 1 | | affinity | Pod affinity for the Ratify deployment | `{}` | | tolerations | Pod tolerations for the Ratify deployment | `[]` | +| env | Environment variables for Ratify container | `[]` | | notationCerts | An array of public certificate/certificate chain used to create inline certstore used by Notation verifier | `` | | cosignKeys | An array of public keys used to create inline key management providers used by Cosign verifier | `[]` | | notation.enabled | Enables/disables the built-in notation verifier. MUST be set to true for notation verification. | `true` | diff --git a/charts/ratify/templates/deployment.yaml b/charts/ratify/templates/deployment.yaml index 7a979ca43..46ed544ae 100644 --- a/charts/ratify/templates/deployment.yaml +++ b/charts/ratify/templates/deployment.yaml @@ -110,6 +110,9 @@ spec: readOnly: true {{- end }} env: + {{- with .Values.env }} + {{- toYaml . | nindent 12 }} + {{- end }} {{- if .Values.logger.level }} - name: RATIFY_LOG_LEVEL value: {{ .Values.logger.level }} diff --git a/charts/ratify/values.yaml b/charts/ratify/values.yaml index ee7c82d41..348736e9b 100644 --- a/charts/ratify/values.yaml +++ b/charts/ratify/values.yaml @@ -169,4 +169,9 @@ akvCertConfig: # DEPRECATED: Use azurekeyvault instead cert2Name: # DEPRECATED: Use azurekeyvault.certificates instead cert2Version: # DEPRECATED: Use azurekeyvault.certificates instead certificates: # DEPRECATED: Use azurekeyvault.certificates instead - tenantId: # DEPRECATED: Use azurekeyvault.tenantId instead \ No newline at end of file + tenantId: # DEPRECATED: Use azurekeyvault.tenantId instead + +# env: environment variables for ratify container +env: [] +# - name: https_proxy +# value: http://proxy-server:80 diff --git a/docs/design/Certificate Revocation Lists.md b/docs/design/Certificate Revocation Lists.md new file mode 100644 index 000000000..40433aa66 --- /dev/null +++ b/docs/design/Certificate Revocation Lists.md @@ -0,0 +1,217 @@ +# CRL and CRL Cache Design + +## Intro + +Certificate validation is an essential step during signature validation. Currently Ratify supports checking for revoked certificates through OCSP supported by notation-go library. However, OCSP validation requires internet connection for each validation while CRL could be cached for better performance. As notary-project is adding the CRL support for notation signature validation, Ratify could utilize it. + +OCSP URLs can be obtained from the certificate's authority information access (AIA) extension as defined in RFC 6960. If the certificate contains multiple OCSP URLs, then each URL is invoked in sequential order, until a 2xx response is received for any of the URL. For each OCSP URL, wait for a default threshold of 2 seconds to receive an OCSP response. The user may be able to configure this threshold. If OCSP response is not available within the timeout threshold the revocation result will be "revocation unavailable". + +If both OCSP URLs and CDP URLs are present, then OCSP is preferred over CRLs. If revocation status cannot be determined using OCSP because of any reason such as unavailability then fallback to using CRLs for revocation check. + +## Goals + +CRL support, including CRL downloading, validation, and revocation list checks. + +- Define a cache provider interface for CRL +- Implement default file-based cache implementation for both CLI and K8S +- Implement preload CRL when cert added from KMP +- Test plan for the CRL feature and performance test between CRL w/o cache. +- Update CRL and CRL caching related documentation + +## Design Points + +**How to Get CRL** + +With no extra configuration, CRL download location (URL) can be obtained from the certificate's CRL Distribution Point (CDP) extension. If the CRL cannot be downloaded within the timeout threshold the revocation result will be "revocation unavailable". More details are showing in the Download CRL section + +**Why Caching** + +Preload CRL can help improve the performance verifier from download CRLs when a single CRL can be up to 32MiB. Prefer ratify cache for reuse the cache provider [interface](https://github.com/ratify-project/ratify/blob/dev/pkg/cache/api.go). Reusing interfaces reduces redundant expressions, helps you easily maintain application objects. + +This design prefer file-based cache over memory-based one to ensure cache would still be available after service restarted. And in CLI scenario memory-based cache would not be applied. +Besides, by using memory-based cache, memory consumption and further cache expiring would increase the design complexity. + +**Why Refresh CRL Cache** + +A CRL is considered expired if the current date is after the `NextUpdate` field in the CRL. Verify that the CRL is valid (not expired) is necessary for revocation check. Monitoring and refreshing CRL on a regular basis can help avoid CRL download timetaken when doing verification by ensure the CRL is valid. + +## Proposed Design + +![image](../img/CRL/CRL-workflow.png) + + +**Ratify Verification Request Path**: + +Step 1: Apply the CRs including certs and CRL config + +Step 2: Load CRLs from cert provided URLs // Implement CanCacheCRL() with GetCertificates() which includes all scenarios that introduce a new cert: KMP, KMP refresher and Verifer Config + +Step 3: Trigger Refresh Monitor and set up refresh schedule // Refresher is based on build-in ticker + +Step 4: Start verify task // Revocation list check is handled by notation verifier. + +Step 5: Load trust policy // Get `Opt.Fetcher` + +Step 6: Load CRL cache + +**CRL Handler**: + +Step 1: Load cert URLs from `[]*x509.Certificate` + +Step 2: Download CRL + +Step 3: Trigger Refresh Monitor, refresh monitor is [`time`](https://pkg.go.dev/time#example-NewTicker) pkg based. + +### Cache Content Design + +Key: +- `uri` in type `string` + +Value: +- `*Bundle` + - `NextUpdate` // nextUpdate can be get from bundle.BaseCRL.NextUpdate + +Reference: [revocation/crl/bundle.go](https://github.com/notaryproject/notation-core-go/blob/main/revocation/crl/bundle.go) + +Check CRL Cache Validity +``` +// directly checks CRL validity +now := time.Now() +if !crl.NextUpdate.IsZero() && now.After(crl.NextUpdate) { + // perform refresh +} +``` + +### Load CRL Cache + +Load cache is triggerred after cert loaded from the either configurations. + +#### Download CRL + + +Download is implemented by CRL `fetcher`, which can be done in parallel via start tasks in seperate go routines. + +CRL download location (URL) can be obtained from the certificate's CRL Distribution Point (CDP) extension. +`notation-core-go` will download all CDP URLs because each CDP URL may belong to a different scope, and we cannot distinguish them. + +For each CDP location, Notary Project verification workflow will try to download the CRL for the default threshold of 5 seconds. Ratify is able to configure this threshold. If the CRL cannot be downloaded within the timeout threshold the revocation result will be "revocation unavailable". + +#### Save CRL to Cache + +``` +// Set stores the CRL bundle in the file system +// Check closest expired date and set to `CRLCacheProvider`. +// Save to temp file and avoid concurrency issue with atomic write operation +// `rename()` is atomic on UNIX-like platforms + + +// notation-go FScache + +type fileCacheContent struct { + // BaseCRL is the ASN.1 encoded base CRL + BaseCRL []byte `json:"baseCRL"` + + // DeltaCRL is the ASN.1 encoded delta CRL + DeltaCRL []byte `json:"deltaCRL,omitempty"` +} + +// This cache builds on top of the UNIX file system to leverage the file system's +// atomic operations. The `rename` and `remove` operations will unlink the old +// file but keep the inode and file descriptor for existing processes to access +// the file. The old inode will be dereferenced when all processes close the old +// file descriptor. Additionally, the operations are proven to be atomic on +// UNIX-like platforms, so there is no need to handle file locking. +// +// NOTE: For Windows, the `open`, `rename` and `remove` operations need file +// locking to ensure atomicity. The current implementation does not handle +// file locking, so the concurrent write from multiple processes may be failed. +// Please do not use this cache in a multi-process environment on Windows. +``` + +### Provide CRL Cache + +#### Get Cache from Provider + +``` +// Get retrieves the CRL bundle from the file system +// If the CRL is expired, return ErrCacheMiss + + +// Policy that uses custom verification level to relax the strict verification. +// It logs expiry and skips the revocation check. +"name": "use-expired-blobs", + "signatureVerification": { + "level" : "strict", + "override" : { + "expiry" : "log", + "revocation" : "skip" + } + }, + +``` +Reference: https://github.com/notaryproject/specifications/blob/main/specs/trust-store-trust-policy.md#signatureverification-details:~:text=Notary%20Project%20defines,scope%20(*). + +#### Refresh Cache + +Cache Data Structure: Store data along with expiration timestamps. + +Monitor Scheduler: Use a scheduler (e.g., time.Ticker) to check the cache at regular intervals. + +Concurrency: Use synchronization mechanisms like mutexes for thread-safe access to shared data. + +Expiration Handling: When setting cache, check closest expired date that set to `CRLCacheProvider`. Compare the current time with the cache item's expiration. If expired, trigger the fetch process to update the data. + +Error Handling and Retries: Implement error handling with retry logic in case of failed refresh operations. + +Use synchronization primitives like mutexes to ensure thread safety during cache updates. + +# Dev Work Items + +- Implement CRL Fetcher based on notation-go library (~ 1-2 weeks) +- Implement file-based CRL Cache Provider (~ 2-3 weeks) +- Support loading CRLs from cert provided URLs, engage PM discussion (~ 1-2 weeks) +- Verifier performance test and Cache r/w performance test (~ 2-3 weeks) +- New e2e tests for different scenarios (~ 1 week) + + +# More details + +**Brief Aside about CRL and CRL Cache** + +X.509 defines one method of certificate revocation. This method involves each CA periodically issuing a signed data structure called a certificate revocation list (CRL). + +A CRL is a time stamped list identifying revoked certificates which is signed by a CA or CRL issuer and made freely available in a public repository. Each revoked certificate is identified in a CRL by its certificate serial number. + +When a certificate-using system uses a certificate (e.g., for verifying a remote user's digital signature), that system not only checks the certificate signature and validity but also acquires a suitably-recent CRL and checks that the certificate serial number is not on that CRL. +The meaning of "suitably-recent" may vary with local policy, but it usually means the most recently-issued CRL. + +A new CRL is issued on a regular periodic basis (e.g., hourly, daily, or weekly). +An entry is added to the CRL as part of the next update following notification of revocation. An entry MUST NOT be removed from the CRL until it appears on one regularly scheduled CRL issued beyond the revoked certificate's validity period. + +Implementations of the Notary Project verification specification support only HTTP CRL URLs. + +**Revocation Checking with CRL** + +To check the revocation status of a certificate against CRL, the following steps must be performed: + +1. Verify the CRL signature. +1. Verify that the CRL is valid (not expired). + A CRL is considered expired if the current date is after the `NextUpdate` field in the CRL. +1. Look up the certificate’s serial number in the CRL. + 1. If the certificate’s serial number is listed in the CRL, look for `InvalidityDate`. + If CRL has an invalidity date and artifact signature is timestamped then compare the invalidity date with the timestamping date. + 1. If the invalidity date is before the timestamping date, the certificate is considered revoked. + 1. If the invalidity date is not present in CRL, the certificate is considered revoked. + 1. If the CRL is expired and the certificate is listed in the CRL for any reason other than `certificate hold`, the certificate is considered revoked. + 1. If the certificate is not listed in the CRL or the revocation reason is `certificate hold`, a new CRL is retrieved if the current time is past the time in the `NextPublish` field in the current CRL. + The new CRL is then checked to determine if the certificate is revoked. + If the original reason was `certificate hold`, the CRL is checked to determine if the certificate is unrevoked by looking for the `RemoveFromCRL` revocation code. + + +**rfc3280** + +REF: [Internet X.509 Public Key Infrastructure](https://www.rfc-editor.org/rfc/rfc3280#section-1) + +Q: Does the Notary Project trust policy support overriding of revocation endpoints to support signature verification in disconnected environments? + +A: TODO: Update after verification extensibility spec is ready. Not natively supported but a user can configure revocationValidations to skip and then use extended validations to check for revocation. \ No newline at end of file diff --git a/docs/design/kmp-nversions.md b/docs/design/kmp-nversions.md new file mode 100644 index 000000000..afc2421db --- /dev/null +++ b/docs/design/kmp-nversions.md @@ -0,0 +1,106 @@ +# nVersionCount support for Key Management Provider + +Author: Josh Duffney (@duffney) + +Tracked issues in scope: + +- https://github.com/ratify-project/ratify/issues/1751 + +Proposal ref: + +- https://github.com/ratify-project/ratify/blob/dev/docs/proposals/Automated-Certificate-and-Key-Updates.md + +## Problem Statement + +In version 1.3.0 and earlier, Ratify does not support the nVersionCount parameter for Key Management Provider (KMP) resources. This means that when a certificate or key is rotated, Ratify updates the cache with the new version and removes the previous one, which may not suit all use cases. + +For instance, if a user needs to retain the last three versions of a certificate or key in the cache, Ratify cannot meet this requirement without manually adjusting the KMP resource for each new version. + +By supporting nVersionCount, Ratify would allow users to specify how many versions of a certificate or key should be kept in the cache, eliminating the need for manual updates to the KMP resource. + +## Proposed Solution + +To address this challenge, this proposal suggests adding support for the `versionHistory` parameter to the KMP resource in Ratify. This parameter will allow users to specify the number of versions of a certificate or key that should be retained in the cache. + +When a new version of a certificate or key is created, Ratify will check the `versionHistory` parameter to determine how many versions should be retained in the cache. If the number of versions exceeds the specified count, Ratify will remove the oldest version from the cache. + +If a version is disabled, Ratify will remove it from the cache. This ensures that disabled versions are not retained in the cache, reducing the risk of using compromised keys or certificates being passed to the verifiers. + +Example: AKV KMP resource with `versionHistory` parameter + +```yaml +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: KeyManagementProvider +metadata: + name: keymanagementprovider-akv +spec: + type: azurekeyvault + refreshInterval: 1m + parameters: + vaultURI: https://yourkeyvault.vault.azure.net/ + certificates: + - name: yourCertName + versionHistory: 2 + tenantID: + clientID: +``` + +Example: AKV KMP resource status with multiple versions retained in the cache + +```yaml +Status: + Issuccess: true + Lastfetchedtime: 2024-10-02T14:58:54Z + Properties: + Certificates: + Last Refreshed: 2024-10-02T14:58:54Z + Name: yourCertName + Version: a1b2c3d4e5f67890abcdef1234567890 + Enabled: true + Last Refreshed: 2024-10-02T14:58:54Z + Name: yourCertName + Version: 0ff373a9259c4578a247cfd7861a8805 + Enabled: false +``` + +## Implementation Details + +- Modify the KMP data structure to include the status of the version. + ```go + type KMPMapKey struct { + Name string + Version string + Enabled string // true or false + Created time.Time // Time the version was created used for determining the oldest version + } + ``` +- Add the `versionHistory` parameter to the KMP resource in Ratify. + - ensure the value cannot be less than 0 or a negative number + - default to 2 if not specified by passing an empty value + - maximum value should be (TBD) + - specify the value at the object level within the parameters of the KMP resource. +- Changes to `azurekeyvault` provider: + - support for the `versionHistory` parameter. + - allowing retrieval of multiple versions of certificates or keys. + - remove the oldest version from the cache when the number of versions exceeds the `versionHistory` parameter. + - update disabled certs status in the cache & remove the certData from the cache. +- Log when the status of a version changes. +- Log when a conflict between the `versionHistory` and the number of specified certificate versions occurs. + +## Dev Work Items + +## Open Questions + +- If a version is disabled, should it be removed from the cache or retained based on the nVersionCount and marked as inactive\disabled? + - [x] Keep the disabled version in the cache and mark it as disabled. +- If a version is disabled, does that count towards the nVersionCount? For example, if nVersionCount is set to 3 and one of the versions is disabled, should Ratify retain the last three active versions or the last three versions, regardless of their status? + - [x] Yes, disabled versions should count towards the nVersionCount. The reason for this is that disabled versions may be re-enabled in the future, and it is important to retain them in the cache. +- Should the existing KMP data structure be changed to group versions by key or certificate name? + - [x] No, a flat list of versions is sufficient. At this time, there is no need to group versions by key or certificate name because the verifiers do not need to know the history of the versions. +- Should the KMP status return a flat list of versions? + - [x] Yes, the status should return a flat list of versions. + +## Future Considerations + +- What should the maximum value for nVersionCount be? + - [ ] TBD diff --git a/docs/img/CRL/CRL-workflow.png b/docs/img/CRL/CRL-workflow.png new file mode 100644 index 000000000..b33faef39 Binary files /dev/null and b/docs/img/CRL/CRL-workflow.png differ diff --git a/docs/proposals/Tag-Digest-CoExist.md b/docs/proposals/Tag-Digest-CoExist.md new file mode 100644 index 000000000..1b8e6d5d7 --- /dev/null +++ b/docs/proposals/Tag-Digest-CoExist.md @@ -0,0 +1,93 @@ +# Ratify Mutation: mutual existence of tags and digest. + +## Problems + +Current User scenarios: +1. I want to see which version of my image is deployed but I can only see the digest in the pod image. + 1. This results in the engineer’s time being wasted on manually mapping between the image digest and the version from the image repository. + 1. All my observability dashboards rely on tags but now I need to manually know which tags belong to which digests and somehow also make my dashboards map between those. This is a big hassle. + +## Solution Overview + +The current solution has been chosen on the basis that Ratify is only meant to mutate from tags to digest as tags are mutable and digests are the ultimate source of truth. This solution is also prepared with the community recommendation of using digests instead of tags in mind with digests being the ultimate source of truth for artifact verification. + +However with the digest-only approach having altercations with broader software engineers NOT focused towards security, embedding digests alongside pre-existing tags in the K8s object spec during mutation as a debug-friendly and engineer friendly way forward seems feasible. As the end container orchestration framework such as `containerd` and ultimately `runc` still continue to rely on only the mutated digest to create containers, engineers on the other hand can rely on the pre-existing and untouched tag in the deployed object (Deployment, Pod, StatefulSet etc)’s image spec to know their source of truth for debugging purposes. + +As discussed in the corresponding [Github issue](https://github.com/ratify-project/ratify/issues/1657), having both tag & digest (`:@`) is NOT a recommended option but retains status for backward compatibility, new options with default configuration adhering to this shall be discussed in the design section. + +## Solution Design and Configurations / Proposed Changes + +This section discusses the various additions to the current Ratify Mutator and the corresponding Helm configuration that can help resolve this problem. We also discuss features that the mutator could incorporate to facilitate other additional problems but is a topic of further discussion and debate not within the realm and scope of the problem at hand. + +The solution proposes tweaks to the following sections to fix the problem at hand: +1. The helm chart shall be changed to add the corresponding new configs facilitating these new features for mutation. +1. The new configs shall be respectively passed to `/app/ratify serve` called through the `deployment.yaml` file to then handle the additional configs. +1. These configs then trickle down to the corresponding mutation code block to handle the mutations according to those config. + +### Configurations + +A new config block shall be added in the helm chart’s `values.yaml` which will be used respectively. + +This feature will be available through the new `provider.mutation` config block which will make the `provider.enableMutation` option obsolete. A new boolean sub-config of `provider.mutation.enable` will be added to facilitate this existing feature. + +A new sub config `mutationStyle` will be added to facilitate the type of mutation the user owuld want. +The following sub-options will be added to incorporate additional configuration during mutation. + +| `mutationStyle` | Implemented / Designed in the current solution? | Summary | Incoming Spec Condition | Upstream calls for Subject Descriptor? | Default Option | +| ----------- | ----------------------------------------------- | ------- | ----------------------- | -------------------------------------- | -------------- | +| `retain-mutated-tag` | Y | Retain the pre-existing tag during mutation / Do not strip the tag away if both tag & digest pre-exists |Contains Tag, Does not contain Digest / Contains Tag, Contains Digest | Y / N | false | +| `digest-only` | Y | Mutate tag to digest, stripping the tag away | Contains Tag, Does not contain Digest / Contains Tag, Contains Digest | Y / N | true | + +The options can work in conjunction to provide the required mutation output. +Here, + +`Latest` tag’s digest = `xxxx` + +`v1.2.4` tag’s digest = `yyyy` + + +| Config | Input | Output | +| ------ | ----- | ------ | +| `mutationStyle: "digest-only"` | docker.io/nginx | docker.io/nginx@sha256:xxxx | +| | docker.io/nginx:latest | docker.io/nginx@sha256:xxxx | +| | docker.io/nginx:v1.2.4 | docker.io/nginx@sha256:yyyy | +| | docker.io/nginx:latest@sha256:xxxx | docker.io/nginx@sha256:xxxx | +| | docker.io/nginx:v1.2.4@sha256:yyyy | docker.io/nginx@sha256:yyyy | +| | docker.io/nginx@sha256:xxxx | docker.io/nginx@sha256:xxxx | +| `mutationStyle: "retain-mutated-tag"` | docker.io/nginx | docker.io/nginx:latest@sha256:xxxx | +| | docker.io/nginx:v1.2.4 | docker.io/nginx:v1.2.4@sha256:yyyy | +| | docker.io/nginx:latest@sha256:xxxx | docker.io/nginx:latest@sha256:xxxx | +| | docker.io/nginx:v1.2.4@sha256:yyyy | docker.io/nginx:v1.2.4@sha256:yyyy | +| | docker.io/nginx@sha256:xxxx | docker.io/nginx@sha256:xxxx | + +An enum style config has been proposed so it does not overcrowd the `provider.mutation` block. Both, addition of new mutation styles as well as parsing on the code side will be easier with this approach. + +### Implementation + +The `mutationStyle` config will be implemented to retain the tag in the resulting spec image. The default option for this config will be `digest-only` to keep supporting the existing config parameter. + +Options of provider.mutation.enable and provider.mutation.retainMutatedTag shall be added into Helm. +Example: + +``` +provider: + tls: + crt: "" +... + mutation: + // enable: true + mutationStyle: "digest-only" // (default), other options are "retain-mutated-tag" + enableMutation: true // deprecated, enable and use mutation.enabled instead. If both are used, `mutation.enable` will be preferred +``` + +The `retain-mutated-tag` option will be available for anyone wanting to control if they want to completely remove tags (the default) or have both tags + digest in the resulting output. + +## Performance Impact +The solution should have very little performance impact considering addition of code will not have any network connectivity related feature. Addition of code mostly should adhere to if-else clauses and other small regex additions. + +## Security Considerations +As the change is purely beautification in nature, no security impact could be thought of. +As long as research suggests all major container orchestration frameworks (docker, podman, etc) support the `tag@digest`, however it’s not guaranteed that it’ll work with other smaller frameworks where this hasn’t yet been implemented. + +## Backward Compatibility +The added config won’t be backward compatible if mutation has been disabled, i.e `enableMutation:false` in the helm chart. This means that a new `provider.mutation.enable` will need to be added in the updated helm charts. diff --git a/docs/proposals/Verify-Latest-N-Artifacts.md b/docs/proposals/Verify-Latest-N-Artifacts.md new file mode 100644 index 000000000..efe7240eb --- /dev/null +++ b/docs/proposals/Verify-Latest-N-Artifacts.md @@ -0,0 +1,103 @@ +# Verify only the latest N artifacts + +## Problem/Motivation + +When configuring a verifier in Ratify, we set the artifact type the verifier should work on. In such case, Ratify will verify all referrers of a given subject that have a matching artifact type using the verifier. +In some cases, this could lead to a wrong behavior. For instance, Vulnerability Artifacts are outdated once a new artifact is written to the repository, as such there is no use for verifying both the new one and the old one. + +The issue with verifying all the matching artifacts could also lead to performance issues, each "verification" process hides within a request to pull the artifcat manifest, and the blobs containing the actual data. +In previous studies made by the ratify team, it was observed that opverloading the registry with requests could lead to errors and throtteling. (see: https://ratify.dev/docs/reference/performance) + +Given the performance study listed above, in order to provide the best experience for Ratify's users ratify would reduce the load it generates on an the registry, thus reducing the chance for throtteling. + +# Proposed Solution + +Ratify uses the Referrer API (see: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers) in order to obtain the list of attached artifacts of a given subject artifact. The response body for this request is a generated OCI image index, that looks like this: + +```json +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 1234, + "digest": "sha256:a1a1a1...", + "artifactType": "application/vnd.example.sbom.v1", + "annotations": { + "org.opencontainers.image.created": "2022-01-01T14:42:55Z", + "org.example.sbom.format": "json" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 1234, + "digest": "sha256:a2a2a2...", + "artifactType": "application/vnd.example.signature.v1", + "annotations": { + "org.opencontainers.image.created": "2022-01-01T07:21:33Z", + "org.example.signature.fingerprint": "abcd" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 1234, + "digest": "sha256:a3a3a3...", + "annotations": { + "org.opencontainers.image.created": "2023-01-01T07:21:33Z", + } + } + ] +} +``` + +The image index response is requried by API to include the image annotations, which gives Ratify to oprrotunity to perform some basic filtration before invoking the verifier on the listed artifacts. The exact mechanism for requiring the filtration should be specific to each verifier as the behavior and logic differs based on is actually being attested, as such, it will be part of the verifier configuration. + +## Latest N artifacts only verification + +Assuming artifacts are generated by ORAS, they all have a `org.opencontainers.image.created` annotation, that markes the creation date of the artifact. Based on it, Ratify could filter out stale artifacts and only evaluate the latest image. To achieve this, Ratify would have to read all the referrers, ordering them based on the artifact age, and only pass the latest one to the corresponding verifier. + +This kind of filtering strategy is best used on artifacts that are rapidly changing, for example Vulnerability Assessment artifacts that are immedietly oudated once a new artifact is pushed to the registry. + +* An artifact without the creation annotation is considered to be the oldest. +* The annotation value should be a date time string in RFC 3339 format, any other value will result is invalid, and should be treated as the oldest artifact. + +## User experiences + +This section describes the experience that users interact with Ratify using the proposed solution. In summary, the propsed solution suggest we should allow for filtering of artifact based on annotations, and as such the following section describes how the customer would configure the filtration. + +Seeing that filteration is unique to each verifier, it should be configured in the verifier itself, as such, in order to maintain backwards compatability, it is important to note that if no filteration is configured the default behavior would be to evaluate all artifacts. + +### Defining artifact age based filtering + +To support artifact age based filtering, we would add an additional field to the verifier configuration: + +```yaml +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: Verifier # NamespacedVerifier has the same spec. +metadata: + name: test-verifier +spec: + name: # REQUIRED: [string], the unique type of the verifier (notation, cosign) + artifactType: # REQUIRED: [string], comma seperated list, artifact type this verifier handles + verifyLastNArtifacts: # Optional: [int], denote the number of attached artfacts that should be verified. only the Last n will be verified. if not defined, all artifacts will be verified. + address: # OPTIONAL: [string], Plugin path, defaults to value of env "RATIFY_CONFIG" or "~/.ratify/plugins" + version: # OPTIONAL: [string], Version of the external plugin, defaults to 1.0.0. On ratify initialization, the specified version will be validated against the supported plugin version. + source: + artifact: # OPTIONAL: [string], Source location to download the plugin binary, learn more at docs/reference/dynamic-plugins.md e.g. wabbitnetworks.azurecr.io/test sample-verifier-plugin:v1 + parameters: # OPTIONAL: [object] Parameters specific to this verifier +``` + +### Implementation Considerations +To implmenet "Last N" verification only, Ratify has to be aware of all the attached artifacts of a givan kind before handing them to the verifier that wishes to attest only the latest artifact. In order to implement such behavior some modification has to be made to the executor of Ratify and the verifier implementation. + +Below are two proposals which are currently being considered for implemetation. +| Approach | Pros | Cons| Notes | +| -------- | ---- | ----| ----- | +| 1. Obtain and store all the referrer list
2. Sort it in descending order.
3. Use the CanVerify method of the referrer to make sure a verifier
that only wants the latest artifact is invoked once. | Naive implementation.

Does not make a huge change in the executor, other than fetching the list before hand.

Transparent change for verifiers that do not wish to use verify the latest image only. | The referrer list can be of any arbitrary size, therefore fetching the entire list may cause Ratify to hit a hard memory limit and crash.
To implement the feature with this kind of behavior, Ratify would have to limit the number of attached artifacts it supports to some constant number which will be determined during the implementation.

Additional latency for sorting the artifacts. | A test index list, with ~1000 artifacts within and two annotations (created timestamp, and another text field) weighs around ~400K, default ratify installation has 512MB of ram, so we're well within the limits of 'normal' use. +| 1. Split verifiers into two groups, those which require only latest artifact, and those which operate on all artifacts.
2. For verifiers that work on all artifacts, no change will be made.
3. For verifiers that require only the last N artifacts, the executor will manage a map between the verifier and an artifact descriptor list that is the "current candidates" for being the latest.
4. As we iterate all the referrer, the cadndiate list is constantly being updated, if a new artifact is discovered.
Once the executor had finished iterating over the referrer list, it would execute all the verifiers that required the latest N artifact against the "current candidate" list for each verifier, which are promised to be latest artifacts. | Does not modify Ratify's current scalability.| Requires the executor to be aware of verifier type, possibly by an interface change on the verifier API

Requires changes in multiple places in the executor, performing the verifier loop another time for the second list of verifiers that only require latest artifact. | The benefit of not pulling all the referrers, from the standpoint of keeping the same 'memory footprint' is not clear. + +# References + +* [Ratify Performance at Scale Study](https://ratify.dev/docs/reference/performance) +* [Referrer API in Distribution Spec](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers) \ No newline at end of file diff --git a/go.mod b/go.mod index 7256e34b5..be3607472 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ratify-project/ratify -go 1.22.5 +go 1.22.8 // Accidentally published prior to 1.0.0 release retract ( @@ -10,12 +10,13 @@ retract ( require ( github.com/Azure/azure-sdk-for-go v68.0.0+incompatible - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 + github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.2 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 - github.com/aws/aws-sdk-go-v2 v1.31.0 - github.com/aws/aws-sdk-go-v2/config v1.27.36 - github.com/aws/aws-sdk-go-v2/credentials v1.17.34 + github.com/aws/aws-sdk-go-v2 v1.32.2 + github.com/aws/aws-sdk-go-v2/config v1.27.43 + github.com/aws/aws-sdk-go-v2/credentials v1.17.41 github.com/aws/aws-sdk-go-v2/service/ecr v1.28.6 github.com/cespare/xxhash/v2 v2.3.0 github.com/dapr/go-sdk v1.8.0 @@ -39,7 +40,7 @@ require ( github.com/owenrumney/go-sarif/v2 v2.3.3 github.com/pkg/errors v0.9.1 github.com/sigstore/cosign/v2 v2.2.4 - github.com/sigstore/sigstore v1.8.9 + github.com/sigstore/sigstore v1.8.10 github.com/sirupsen/logrus v1.9.3 github.com/spdx/tools-golang v0.5.5 github.com/spf13/cobra v1.8.1 @@ -48,7 +49,7 @@ require ( go.opentelemetry.io/otel/metric v1.28.0 go.opentelemetry.io/otel/sdk/metric v1.27.0 golang.org/x/sync v0.8.0 - google.golang.org/grpc v1.66.2 + google.golang.org/grpc v1.66.3 google.golang.org/protobuf v1.34.2 k8s.io/api v0.28.14 k8s.io/apimachinery v0.28.14 @@ -82,7 +83,7 @@ require ( github.com/aliyun/credentials-go v1.3.1 // indirect github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.31.3 // indirect github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect @@ -118,6 +119,7 @@ require ( github.com/sigstore/timestamp-authority v1.2.2 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/thales-e-security/pool v0.0.2 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect @@ -130,7 +132,7 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect @@ -139,15 +141,15 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.23.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.31.0 // indirect - github.com/aws/smithy-go v1.21.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/bshuster-repo/logrus-logstash-hook v1.1.0 @@ -202,7 +204,7 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -234,14 +236,14 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.6.0 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 28ceffc59..ecb26633c 100644 --- a/go.sum +++ b/go.sum @@ -18,12 +18,14 @@ github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0/go.mod h1:GgeIE+1be8Ivm7Sh4RgwI42aTtC9qrcj+Y9Y6CjJhJs= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.2 h1:wBx10efdJcl8FSewgc41kAW4AvHPgmJZmN7fpNxn8rc= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.2/go.mod h1:zzmu18cpAinSbhC86oWd47nmgbb91Fl+Yac2PE8NdYk= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= @@ -125,38 +127,38 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.51.6 h1:Ld36dn9r7P9IjU8WZSaswQ8Y/XUCRpewim5980DwYiU= github.com/aws/aws-sdk-go v1.51.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= -github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= -github.com/aws/aws-sdk-go-v2/config v1.27.36 h1:4IlvHh6Olc7+61O1ktesh0jOcqmq/4WG6C2Aj5SKXy0= -github.com/aws/aws-sdk-go-v2/config v1.27.36/go.mod h1:IiBpC0HPAGq9Le0Xxb1wpAKzEfAQ3XlYgJLYKEVYcfw= -github.com/aws/aws-sdk-go-v2/credentials v1.17.34 h1:gmkk1l/cDGSowPRzkdxYi8edw+gN4HmVK151D/pqGNc= -github.com/aws/aws-sdk-go-v2/credentials v1.17.34/go.mod h1:4R9OEV3tgFMsok4ZeFpExn7zQaZRa9MRGFYnI/xC/vs= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.27.43 h1:p33fDDihFC390dhhuv8nOmX419wjOSDQRb+USt20RrU= +github.com/aws/aws-sdk-go-v2/config v1.27.43/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/ecr v1.28.6 h1:CnQNpQv+WGl5aECyAXrJ4w+Qccz2aC/uXg2OjxiPl30= github.com/aws/aws-sdk-go-v2/service/ecr v1.28.6/go.mod h1:1FKdZMR/Tfx40IKjdLDRlFz/UKlff8CKQuC7mhlTAMM= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.7 h1:dsmihXaPkhFuUTiL+ygm9RtUYEmhOeIl7DXNIHCoKDg= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.7/go.mod h1:g7If3uXj+mKcmIuxh08qh8I9ju6f/aOSWMyc6hEEi58= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= github.com/aws/aws-sdk-go-v2/service/kms v1.31.3 h1:wLBgq6nDNYdd0A5CvscVAKV5SVlHKOHVPedpgtigATg= github.com/aws/aws-sdk-go-v2/service/kms v1.31.3/go.mod h1:8lETO9lelSG2B6KMXFh2OwPPqGV6WQM3RqLAEjP1xaU= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.0 h1:fHySkG0IGj2nepgGJPmmhZYL9ndnsq1Tvc6MeuVQCaQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.0/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0 h1:cU/OeQPNReyMj1JEBgjE29aclYZYtXcsPMXbTkVGMFk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.0 h1:GNVxIHBTi2EgwCxpNiozhNasMOK+ROUA2Z3X+cSBX58= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.0/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= -github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= -github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8/go.mod h1:2JF49jcDOrLStIXN/j/K1EKRq8a8R2qRnlZA6/o/c7c= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -575,8 +577,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= @@ -613,8 +615,8 @@ github.com/sigstore/fulcio v1.4.5 h1:WWNnrOknD0DbruuZWCbN+86WRROpEl3Xts+WT2Ek1yc github.com/sigstore/fulcio v1.4.5/go.mod h1:oz3Qwlma8dWcSS/IENR/6SjbW4ipN0cxpRVfgdsjMU8= github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= -github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= -github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= +github.com/sigstore/sigstore v1.8.10 h1:r4t+TYzJlG9JdFxMy+um9GZhZ2N1hBTyTex0AHEZxFs= +github.com/sigstore/sigstore v1.8.10/go.mod h1:BekjqxS5ZtHNJC4u3Q3Stvfx2eyisbW/lUZzmPU2u4A= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g= @@ -657,6 +659,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -780,8 +783,8 @@ golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= @@ -823,11 +826,11 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -871,8 +874,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -883,8 +886,8 @@ golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -897,8 +900,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -941,8 +944,8 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= +google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= 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= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/httpserver/Dockerfile b/httpserver/Dockerfile index 4ba69f179..33e3ccc0b 100644 --- a/httpserver/Dockerfile +++ b/httpserver/Dockerfile @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM --platform=$BUILDPLATFORM golang:1.22@sha256:4594271250150c1a322ed749abfd218e1a8c6eb1ade90872e325a664412e2037 as builder +FROM --platform=$BUILDPLATFORM golang:1.22@sha256:0ca97f4ab335f4b284a5b8190980c7cdc21d320d529f2b643e8a8733a69bfb6b as builder ARG TARGETPLATFORM ARG TARGETOS @@ -41,7 +41,7 @@ RUN if [ "$build_licensechecker" = "true" ]; then go build -o /app/out/plugins/ RUN if [ "$build_schemavalidator" = "true" ]; then go build -o /app/out/plugins/ /app/plugins/verifier/schemavalidator; fi RUN if [ "$build_vulnerabilityreport" = "true" ]; then go build -o /app/out/plugins/ /app/plugins/verifier/vulnerabilityreport; fi -FROM gcr.io/distroless/static:nonroot@sha256:dcd3f1f09adef5689088c9c4d96a8d98c889d8281d3946145074f89eafe7e1af +FROM gcr.io/distroless/static:nonroot@sha256:26f9b99f2463f55f20db19feb4d96eb88b056e0f1be7016bb9296a464a89d772 LABEL org.opencontainers.image.source https://github.com/ratify-project/ratify ARG RATIFY_FOLDER=$HOME/.ratify/ diff --git a/pkg/common/oras/authprovider/azure/azureidentity.go b/pkg/common/oras/authprovider/azure/azureidentity.go index 0a5a00e5c..d0369c4dc 100644 --- a/pkg/common/oras/authprovider/azure/azureidentity.go +++ b/pkg/common/oras/authprovider/azure/azureidentity.go @@ -29,14 +29,44 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/runtime/2019-08-15-preview/containerregistry" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" ) +// ManagedIdentityTokenGetter defines an interface for getting a managed identity token. +type ManagedIdentityTokenGetter interface { + GetManagedIdentityToken(ctx context.Context, clientID string) (azcore.AccessToken, error) +} + +// defaultManagedIdentityTokenGetterImpl is the default implementation of getManagedIdentityToken. +type defaultManagedIdentityTokenGetterImpl struct{} + +func (g *defaultManagedIdentityTokenGetterImpl) GetManagedIdentityToken(ctx context.Context, clientID string) (azcore.AccessToken, error) { + return getManagedIdentityToken(ctx, clientID, azidentity.NewManagedIdentityCredential) +} + +func getManagedIdentityToken(ctx context.Context, clientID string, newCredentialFunc func(opts *azidentity.ManagedIdentityCredentialOptions) (*azidentity.ManagedIdentityCredential, error)) (azcore.AccessToken, error) { + id := azidentity.ClientID(clientID) + opts := azidentity.ManagedIdentityCredentialOptions{ID: id} + cred, err := newCredentialFunc(&opts) + if err != nil { + return azcore.AccessToken{}, err + } + scopes := []string{AADResource} + if cred != nil { + return cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: scopes}) + } + return azcore.AccessToken{}, re.ErrorCodeConfigInvalid.WithComponentType(re.AuthProvider).WithDetail("config is nil pointer for GetServicePrincipalToken") +} + type azureManagedIdentityProviderFactory struct{} -type azureManagedIdentityAuthProvider struct { - identityToken azcore.AccessToken - clientID string - tenantID string + +type MIAuthProvider struct { + identityToken azcore.AccessToken + clientID string + tenantID string + authClientFactory AuthClientFactory + registryHostGetter RegistryHostGetter + getManagedIdentityToken ManagedIdentityTokenGetter } type azureManagedIdentityAuthProviderConf struct { @@ -53,7 +83,7 @@ func init() { provider.Register(azureManagedIdentityAuthProviderName, &azureManagedIdentityProviderFactory{}) } -// Create returns an azureManagedIdentityAuthProvider +// Create returns an MIAuthProvider func (s *azureManagedIdentityProviderFactory) Create(authProviderConfig provider.AuthProviderConfig) (provider.AuthProvider, error) { conf := azureManagedIdentityAuthProviderConf{} authProviderConfigBytes, err := json.Marshal(authProviderConfig) @@ -80,20 +110,22 @@ func (s *azureManagedIdentityProviderFactory) Create(authProviderConfig provider return nil, err } // retrieve an AAD Access token - token, err := getManagedIdentityToken(context.Background(), client) + token, err := getManagedIdentityToken(context.Background(), client, azidentity.NewManagedIdentityCredential) if err != nil { return nil, re.ErrorCodeAuthDenied.NewError(re.AuthProvider, "", re.AzureManagedIdentityLink, err, "", re.HideStackTrace) } - return &azureManagedIdentityAuthProvider{ - identityToken: token, - clientID: client, - tenantID: tenant, + return &MIAuthProvider{ + identityToken: token, + clientID: client, + tenantID: tenant, + authClientFactory: &defaultAuthClientFactoryImpl{}, // Concrete implementation + getManagedIdentityToken: &defaultManagedIdentityTokenGetterImpl{}, // Concrete implementation }, nil } // Enabled checks for non empty tenant ID and AAD access token -func (d *azureManagedIdentityAuthProvider) Enabled(_ context.Context) bool { +func (d *MIAuthProvider) Enabled(_ context.Context) bool { if d.clientID == "" { return false } @@ -112,57 +144,58 @@ func (d *azureManagedIdentityAuthProvider) Enabled(_ context.Context) bool { // Provide returns the credentials for a specified artifact. // Uses Managed Identity to retrieve an AAD access token which can be // exchanged for a valid ACR refresh token for login. -func (d *azureManagedIdentityAuthProvider) Provide(ctx context.Context, artifact string) (provider.AuthConfig, error) { +func (d *MIAuthProvider) Provide(ctx context.Context, artifact string) (provider.AuthConfig, error) { if !d.Enabled(ctx) { return provider.AuthConfig{}, fmt.Errorf("azure managed identity provider is not properly enabled") } + // parse the artifact reference string to extract the registry host name - artifactHostName, err := provider.GetRegistryHostName(artifact) + artifactHostName, err := d.registryHostGetter.GetRegistryHost(artifact) if err != nil { - return provider.AuthConfig{}, err + return provider.AuthConfig{}, re.ErrorCodeHostNameInvalid.WithComponentType(re.AuthProvider) } // need to refresh AAD token if it's expired if time.Now().Add(time.Minute * 5).After(d.identityToken.ExpiresOn) { - newToken, err := getManagedIdentityToken(ctx, d.clientID) + newToken, err := d.getManagedIdentityToken.GetManagedIdentityToken(ctx, d.clientID) if err != nil { return provider.AuthConfig{}, re.ErrorCodeAuthDenied.NewError(re.AuthProvider, "", re.AzureManagedIdentityLink, err, "could not refresh azure managed identity token", re.HideStackTrace) } d.identityToken = newToken logger.GetLogger(ctx, logOpt).Info("successfully refreshed azure managed identity token") } + // add protocol to generate complete URI serverURL := "https://" + artifactHostName - // create registry client and exchange AAD token for registry refresh token - refreshTokenClient := containerregistry.NewRefreshTokensClient(serverURL) - rt, err := refreshTokenClient.GetFromExchange(ctx, "access_token", artifactHostName, d.tenantID, "", d.identityToken.Token) + // TODO: Consider adding authentication client options for multicloud scenarios + var options *azcontainerregistry.AuthenticationClientOptions + client, err := d.authClientFactory.CreateAuthClient(serverURL, options) + if err != nil { + return provider.AuthConfig{}, re.ErrorCodeAuthDenied.WithError(err).WithDetail("failed to create authentication client for container registry by azure managed identity token") + } + + response, err := client.ExchangeAADAccessTokenForACRRefreshToken( + ctx, + azcontainerregistry.PostContentSchemaGrantType(GrantTypeAccessToken), + artifactHostName, + &azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{ + AccessToken: &d.identityToken.Token, + Tenant: &d.tenantID, + }, + ) if err != nil { return provider.AuthConfig{}, re.ErrorCodeAuthDenied.NewError(re.AuthProvider, "", re.AzureManagedIdentityLink, err, "failed to get refresh token for container registry by azure managed identity token", re.HideStackTrace) } + rt := response.ACRRefreshToken - expiresOn := getACRExpiryIfEarlier(d.identityToken.ExpiresOn) - + refreshTokenExpiry := getACRExpiryIfEarlier(d.identityToken.ExpiresOn) authConfig := provider.AuthConfig{ Username: dockerTokenLoginUsernameGUID, Password: *rt.RefreshToken, Provider: d, - ExpiresOn: expiresOn, + ExpiresOn: refreshTokenExpiry, } return authConfig, nil } - -func getManagedIdentityToken(ctx context.Context, clientID string) (azcore.AccessToken, error) { - id := azidentity.ClientID(clientID) - opts := azidentity.ManagedIdentityCredentialOptions{ID: id} - cred, err := azidentity.NewManagedIdentityCredential(&opts) - if err != nil { - return azcore.AccessToken{}, err - } - scopes := []string{AADResource} - if cred != nil { - return cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: scopes}) - } - return azcore.AccessToken{}, re.ErrorCodeConfigInvalid.WithComponentType(re.AuthProvider).WithDetail("config is nil pointer for GetServicePrincipalToken") -} diff --git a/pkg/common/oras/authprovider/azure/azureidentity_test.go b/pkg/common/oras/authprovider/azure/azureidentity_test.go index 472e704b9..8d466d3d1 100644 --- a/pkg/common/oras/authprovider/azure/azureidentity_test.go +++ b/pkg/common/oras/authprovider/azure/azureidentity_test.go @@ -20,15 +20,31 @@ import ( "errors" "os" "testing" + "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + azcontainerregistry "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" ratifyerrors "github.com/ratify-project/ratify/errors" "github.com/ratify-project/ratify/pkg/common/oras/authprovider" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) +// Mock types for external dependencies +type MockManagedIdentityTokenGetter struct { + mock.Mock +} + +// Mock ManagedIdentityTokenGetter.GetManagedIdentityToken +func (m *MockManagedIdentityTokenGetter) GetManagedIdentityToken(ctx context.Context, clientID string) (azcore.AccessToken, error) { + args := m.Called(ctx, clientID) + return args.Get(0).(azcore.AccessToken), args.Error(1) +} + // Verifies that Enabled checks if tenantID is empty or AAD token is empty func TestAzureMSIEnabled_ExpectedResults(t *testing.T) { - azAuthProvider := azureManagedIdentityAuthProvider{ + azAuthProvider := MIAuthProvider{ tenantID: "test_tenant", clientID: "test_client", identityToken: azcore.AccessToken{ @@ -89,3 +105,168 @@ func TestAzureMSIValidation_EnvironmentVariables_ExpectedResults(t *testing.T) { t.Fatalf("create auth provider should have failed: expected err %s, but got err %s", expectedErr, err) } } + +// Test for invalid configuration when tenant ID is missing +func TestAzureManagedIdentityProviderFactory_Create_NoTenantID(t *testing.T) { + t.Setenv("AZURE_TENANT_ID", "") + + // Initialize factory + factory := &azureManagedIdentityProviderFactory{} + + // Attempt to create MIAuthProvider with empty configuration + _, err := factory.Create(map[string]interface{}{}) + + // Validate the error + assert.Error(t, err) + assert.Contains(t, err.Error(), "AZURE_TENANT_ID environment variable is empty") +} + +// Test for missing client ID +func TestAzureManagedIdentityProviderFactory_Create_NoClientID(t *testing.T) { + t.Setenv("AZURE_TENANT_ID", "tenantID") + t.Setenv("AZURE_CLIENT_ID", "") + + // Initialize factory + factory := &azureManagedIdentityProviderFactory{} + + // Attempt to create MIAuthProvider with empty client ID + _, err := factory.Create(map[string]interface{}{}) + + // Validate the error + assert.Error(t, err) + assert.Contains(t, err.Error(), "AZURE_CLIENT_ID environment variable is empty") +} + +// Test successful token refresh +func TestMIAuthProvider_Provide_TokenRefreshSuccess(t *testing.T) { + // Mock dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockManagedIdentityTokenGetter := new(MockManagedIdentityTokenGetter) + mockAuthClient := new(MockAuthClient) + + // Define token values + expiredToken := azcore.AccessToken{Token: "expired_token", ExpiresOn: time.Now().Add(-10 * time.Minute)} + newTokenString := "refreshed" + newAADToken := azcore.AccessToken{Token: "new_token", ExpiresOn: time.Now().Add(10 * time.Minute)} + refreshToken := azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{ + ACRRefreshToken: azcontainerregistry.ACRRefreshToken{RefreshToken: &newTokenString}, + } + + // Setup mock expectations + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("example.azurecr.io", nil) + mockAuthClientFactory.On("CreateAuthClient", "https://example.azurecr.io", mock.Anything).Return(mockAuthClient, nil) + mockAuthClient.On("ExchangeAADAccessTokenForACRRefreshToken", mock.Anything, azcontainerregistry.PostContentSchemaGrantType(GrantTypeAccessToken), "example.azurecr.io", mock.Anything).Return(refreshToken, nil) + mockManagedIdentityTokenGetter.On("GetManagedIdentityToken", mock.Anything, "clientID").Return(newAADToken, nil) + + // Initialize provider with expired token + provider := MIAuthProvider{ + identityToken: expiredToken, + clientID: "clientID", + tenantID: "tenantID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getManagedIdentityToken: mockManagedIdentityTokenGetter, + } + + // Call Provide method + ctx := context.Background() + authConfig, err := provider.Provide(ctx, "artifact_name") + + // Validate success and token refresh + assert.NoError(t, err) + assert.Equal(t, "refreshed", authConfig.Password) +} + +// Test failed token refresh +func TestMIAuthProvider_Provide_TokenRefreshFailure(t *testing.T) { + // Mock dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockManagedIdentityTokenGetter := new(MockManagedIdentityTokenGetter) + + // Define token values + expiredToken := azcore.AccessToken{Token: "expired_token", ExpiresOn: time.Now().Add(-10 * time.Minute)} + + // Setup mock expectations + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("example.azurecr.io", nil) + mockManagedIdentityTokenGetter.On("GetManagedIdentityToken", mock.Anything, "clientID").Return(azcore.AccessToken{}, errors.New("token refresh failed")) + + // Initialize provider with expired token + provider := MIAuthProvider{ + identityToken: expiredToken, + clientID: "clientID", + tenantID: "tenantID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getManagedIdentityToken: mockManagedIdentityTokenGetter, + } + + // Call Provide method + ctx := context.Background() + _, err := provider.Provide(ctx, "artifact_name") + + // Validate failure + assert.Error(t, err) + assert.Contains(t, err.Error(), "could not refresh azure managed identity token") +} + +// Test for invalid hostname retrieval +func TestMIAuthProvider_Provide_InvalidHostName(t *testing.T) { + // Mock dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockManagedIdentityTokenGetter := new(MockManagedIdentityTokenGetter) + + // Define valid token + validToken := azcore.AccessToken{Token: "valid_token", ExpiresOn: time.Now().Add(10 * time.Minute)} + + // Setup mock expectations for invalid hostname + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("", errors.New("invalid hostname")) + + // Initialize provider with valid token + provider := MIAuthProvider{ + identityToken: validToken, + clientID: "clientID", + tenantID: "tenantID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getManagedIdentityToken: mockManagedIdentityTokenGetter, + } + + // Call Provide method + ctx := context.Background() + _, err := provider.Provide(ctx, "artifact_name") + + // Validate failure + assert.Error(t, err) + assert.Contains(t, err.Error(), "HOST_NAME_INVALID") +} + +// Unit tests +func TestGetManagedIdentityToken(t *testing.T) { + ctx := context.Background() + clientID := "test-client-id" + expectedToken := azcore.AccessToken{Token: "test-token", ExpiresOn: time.Now().Add(time.Hour)} + + mockGetter := new(MockManagedIdentityTokenGetter) + mockGetter.On("GetManagedIdentityToken", ctx, clientID).Return(expectedToken, nil) + + token, err := mockGetter.GetManagedIdentityToken(ctx, clientID) + assert.Nil(t, err) + assert.Equal(t, expectedToken, token) +} + +func TestGetManagedIdentityToken_Error(t *testing.T) { + ctx := context.Background() + clientID := "test-client-id" + + // Mock the newCredentialFunc to return an error + mockNewCredentialFunc := func(_ *azidentity.ManagedIdentityCredentialOptions) (*azidentity.ManagedIdentityCredential, error) { + return nil, assert.AnError + } + + token, err := getManagedIdentityToken(ctx, clientID, mockNewCredentialFunc) + assert.NotNil(t, err) + assert.Equal(t, azcore.AccessToken{}, token) +} diff --git a/pkg/common/oras/authprovider/azure/azureworkloadidentity.go b/pkg/common/oras/authprovider/azure/azureworkloadidentity.go index a40ce4436..31f45127d 100644 --- a/pkg/common/oras/authprovider/azure/azureworkloadidentity.go +++ b/pkg/common/oras/authprovider/azure/azureworkloadidentity.go @@ -21,21 +21,57 @@ import ( "os" "time" + azcontainerregistry "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" re "github.com/ratify-project/ratify/errors" "github.com/ratify-project/ratify/internal/logger" provider "github.com/ratify-project/ratify/pkg/common/oras/authprovider" - "github.com/ratify-project/ratify/pkg/metrics" "github.com/ratify-project/ratify/pkg/utils/azureauth" - "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/runtime/2019-08-15-preview/containerregistry" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" ) +// AADAccessTokenGetter defines an interface for getting an AAD access token. +type AADAccessTokenGetter interface { + GetAADAccessToken(ctx context.Context, tenantID, clientID, resource string) (confidential.AuthResult, error) +} + +// defaultAADAccessTokenGetterImpl is the default implementation of AADAccessTokenGetter. +type defaultAADAccessTokenGetterImpl struct{} + +func (g *defaultAADAccessTokenGetterImpl) GetAADAccessToken(ctx context.Context, tenantID, clientID, resource string) (confidential.AuthResult, error) { + return defaultGetAADAccessToken(ctx, tenantID, clientID, resource) +} + +func defaultGetAADAccessToken(ctx context.Context, tenantID, clientID, resource string) (confidential.AuthResult, error) { + return azureauth.GetAADAccessToken(ctx, tenantID, clientID, resource) +} + +// MetricsReporter defines an interface for reporting metrics. +type MetricsReporter interface { + ReportMetrics(ctx context.Context, duration int64, artifactHostName string) +} + +// defaultMetricsReporterImpl is the default implementation of MetricsReporter. +type defaultMetricsReporterImpl struct{} + +func (r *defaultMetricsReporterImpl) ReportMetrics(ctx context.Context, duration int64, artifactHostName string) { + defaultReportMetrics(ctx, duration, artifactHostName) +} + +func defaultReportMetrics(ctx context.Context, duration int64, artifactHostName string) { + logger.GetLogger(ctx, logOpt).Infof("Metrics Report: Duration=%dms, Host=%s", duration, artifactHostName) +} + type AzureWIProviderFactory struct{} //nolint:revive // ignore linter to have unique type name -type azureWIAuthProvider struct { - aadToken confidential.AuthResult - tenantID string - clientID string + +type WIAuthProvider struct { + aadToken confidential.AuthResult + tenantID string + clientID string + authClientFactory AuthClientFactory + registryHostGetter RegistryHostGetter + getAADAccessToken AADAccessTokenGetter + reportMetrics MetricsReporter } type azureWIAuthProviderConf struct { @@ -78,20 +114,24 @@ func (s *AzureWIProviderFactory) Create(authProviderConfig provider.AuthProvider } // retrieve an AAD Access token - token, err := azureauth.GetAADAccessToken(context.Background(), tenant, clientID, AADResource) + token, err := defaultGetAADAccessToken(context.Background(), tenant, clientID, AADResource) if err != nil { return nil, re.ErrorCodeAuthDenied.NewError(re.AuthProvider, "", re.AzureWorkloadIdentityLink, err, "", re.HideStackTrace) } - return &azureWIAuthProvider{ - aadToken: token, - tenantID: tenant, - clientID: clientID, + return &WIAuthProvider{ + aadToken: token, + tenantID: tenant, + clientID: clientID, + authClientFactory: &defaultAuthClientFactoryImpl{}, // Concrete implementation + registryHostGetter: &defaultRegistryHostGetterImpl{}, // Concrete implementation + getAADAccessToken: &defaultAADAccessTokenGetterImpl{}, // Concrete implementation + reportMetrics: &defaultMetricsReporterImpl{}, }, nil } // Enabled checks for non empty tenant ID and AAD access token -func (d *azureWIAuthProvider) Enabled(_ context.Context) bool { +func (d *WIAuthProvider) Enabled(_ context.Context) bool { if d.tenantID == "" || d.clientID == "" { return false } @@ -106,19 +146,20 @@ func (d *azureWIAuthProvider) Enabled(_ context.Context) bool { // Provide returns the credentials for a specified artifact. // Uses Azure Workload Identity to retrieve an AAD access token which can be // exchanged for a valid ACR refresh token for login. -func (d *azureWIAuthProvider) Provide(ctx context.Context, artifact string) (provider.AuthConfig, error) { +func (d *WIAuthProvider) Provide(ctx context.Context, artifact string) (provider.AuthConfig, error) { if !d.Enabled(ctx) { return provider.AuthConfig{}, re.ErrorCodeConfigInvalid.WithComponentType(re.AuthProvider).WithDetail("azure workload identity auth provider is not properly enabled") } + // parse the artifact reference string to extract the registry host name - artifactHostName, err := provider.GetRegistryHostName(artifact) + artifactHostName, err := d.registryHostGetter.GetRegistryHost(artifact) if err != nil { return provider.AuthConfig{}, re.ErrorCodeHostNameInvalid.WithComponentType(re.AuthProvider) } // need to refresh AAD token if it's expired if time.Now().Add(time.Minute * 5).After(d.aadToken.ExpiresOn) { - newToken, err := azureauth.GetAADAccessToken(ctx, d.tenantID, d.clientID, AADResource) + newToken, err := d.getAADAccessToken.GetAADAccessToken(ctx, d.tenantID, d.clientID, AADResource) if err != nil { return provider.AuthConfig{}, re.ErrorCodeAuthDenied.NewError(re.AuthProvider, "", re.AzureWorkloadIdentityLink, nil, "could not refresh AAD token", re.HideStackTrace) } @@ -129,14 +170,29 @@ func (d *azureWIAuthProvider) Provide(ctx context.Context, artifact string) (pro // add protocol to generate complete URI serverURL := "https://" + artifactHostName - // create registry client and exchange AAD token for registry refresh token - refreshTokenClient := containerregistry.NewRefreshTokensClient(serverURL) + // TODO: Consider adding authentication client options for multicloud scenarios + var options *azcontainerregistry.AuthenticationClientOptions + client, err := d.authClientFactory.CreateAuthClient(serverURL, options) + if err != nil { + return provider.AuthConfig{}, re.ErrorCodeAuthDenied.WithError(err).WithDetail("failed to create authentication client for container registry by azure managed identity token") + } + startTime := time.Now() - rt, err := refreshTokenClient.GetFromExchange(context.Background(), "access_token", artifactHostName, d.tenantID, "", d.aadToken.AccessToken) + response, err := client.ExchangeAADAccessTokenForACRRefreshToken( + ctx, + azcontainerregistry.PostContentSchemaGrantType(GrantTypeAccessToken), + artifactHostName, + &azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{ + AccessToken: &d.aadToken.AccessToken, + Tenant: &d.tenantID, + }, + ) if err != nil { return provider.AuthConfig{}, re.ErrorCodeAuthDenied.NewError(re.AuthProvider, "", re.AzureWorkloadIdentityLink, err, "failed to get refresh token for container registry", re.HideStackTrace) } - metrics.ReportACRExchangeDuration(ctx, time.Since(startTime).Milliseconds(), artifactHostName) + rt := response.ACRRefreshToken + + d.reportMetrics.ReportMetrics(ctx, time.Since(startTime).Milliseconds(), artifactHostName) refreshTokenExpiry := getACRExpiryIfEarlier(d.aadToken.ExpiresOn) authConfig := provider.AuthConfig{ diff --git a/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go b/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go index 3695ef65a..b2ffaa0cd 100644 --- a/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go +++ b/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go @@ -22,14 +22,319 @@ import ( "testing" "time" - "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" ratifyerrors "github.com/ratify-project/ratify/errors" "github.com/ratify-project/ratify/pkg/common/oras/authprovider" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + azcontainerregistry "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" ) +// MockAADAccessTokenGetter for retrieving AAD access token +type MockAADAccessTokenGetter struct { + mock.Mock +} + +func (m *MockAADAccessTokenGetter) GetAADAccessToken(ctx context.Context, tenantID, clientID, resource string) (confidential.AuthResult, error) { + args := m.Called(ctx, tenantID, clientID, resource) + return args.Get(0).(confidential.AuthResult), args.Error(1) +} + +// MockMetricsReporter for reporting metrics +type MockMetricsReporter struct { + mock.Mock +} + +func (m *MockMetricsReporter) ReportMetrics(ctx context.Context, duration int64, artifactHostName string) { + m.Called(ctx, duration, artifactHostName) +} + +// Test for successful Provide function +func TestWIAuthProvider_Provide_Success(t *testing.T) { + // Mock all dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockAADAccessTokenGetter := new(MockAADAccessTokenGetter) + mockMetricsReporter := new(MockMetricsReporter) + mockAuthClient := new(MockAuthClient) + + // Mock AAD token + initialToken := confidential.AuthResult{AccessToken: "initial_token", ExpiresOn: time.Now().Add(10 * time.Minute)} + refreshTokenString := "new_refresh_token" + refreshToken := azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{ + ACRRefreshToken: azcontainerregistry.ACRRefreshToken{RefreshToken: &refreshTokenString}, + } + + // Set expectations for mocked functions + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("example.azurecr.io", nil) + mockAuthClientFactory.On("CreateAuthClient", "https://example.azurecr.io", mock.Anything).Return(mockAuthClient, nil) + mockAuthClient.On("ExchangeAADAccessTokenForACRRefreshToken", mock.Anything, azcontainerregistry.PostContentSchemaGrantType(GrantTypeAccessToken), "example.azurecr.io", mock.Anything).Return(refreshToken, nil) + mockAADAccessTokenGetter.On("GetAADAccessToken", mock.Anything, "tenantID", "clientID", mock.Anything).Return(initialToken, nil) + mockMetricsReporter.On("ReportMetrics", mock.Anything, mock.Anything, "example.azurecr.io").Return() + + // Create WIAuthProvider + provider := WIAuthProvider{ + aadToken: initialToken, + tenantID: "tenantID", + clientID: "clientID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getAADAccessToken: mockAADAccessTokenGetter, + reportMetrics: mockMetricsReporter, + } + + // Call Provide method + ctx := context.Background() + authConfig, err := provider.Provide(ctx, "artifact_name") + + // Assertions + assert.NoError(t, err) + assert.Equal(t, "new_refresh_token", authConfig.Password) +} + +// Test for AAD token refresh logic +func TestWIAuthProvider_Provide_RefreshToken(t *testing.T) { + // Mock all dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockAADAccessTokenGetter := new(MockAADAccessTokenGetter) + mockMetricsReporter := new(MockMetricsReporter) + mockAuthClient := new(MockAuthClient) + + // Mock expired AAD token, and refreshed token + expiredToken := confidential.AuthResult{AccessToken: "expired_token", ExpiresOn: time.Now().Add(-10 * time.Minute)} + newToken := confidential.AuthResult{AccessToken: "new_token", ExpiresOn: time.Now().Add(10 * time.Minute)} + refreshTokenString := "refreshed_token" + refreshToken := azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{ + ACRRefreshToken: azcontainerregistry.ACRRefreshToken{RefreshToken: &refreshTokenString}, + } + + // Set expectations for mocked functions + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("example.azurecr.io", nil) + mockAuthClientFactory.On("CreateAuthClient", "https://example.azurecr.io", mock.Anything).Return(mockAuthClient, nil) + mockAuthClient.On("ExchangeAADAccessTokenForACRRefreshToken", mock.Anything, azcontainerregistry.PostContentSchemaGrantType(GrantTypeAccessToken), "example.azurecr.io", mock.Anything).Return(refreshToken, nil) + mockAADAccessTokenGetter.On("GetAADAccessToken", mock.Anything, "tenantID", "clientID", mock.Anything).Return(newToken, nil) + mockMetricsReporter.On("ReportMetrics", mock.Anything, mock.Anything, "example.azurecr.io").Return() + + // Create WIAuthProvider with expired token + provider := WIAuthProvider{ + aadToken: expiredToken, + tenantID: "tenantID", + clientID: "clientID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getAADAccessToken: mockAADAccessTokenGetter, + reportMetrics: mockMetricsReporter, + } + + // Call Provide method + ctx := context.Background() + authConfig, err := provider.Provide(ctx, "artifact_name") + + // Assertions + assert.NoError(t, err) + assert.Equal(t, "refreshed_token", authConfig.Password) +} + +// Test for failure when GetAADAccessToken fails +func TestWIAuthProvider_Provide_AADTokenFailure(t *testing.T) { + // Mock all dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockAADAccessTokenGetter := new(MockAADAccessTokenGetter) + mockMetricsReporter := new(MockMetricsReporter) + + // Mock expired AAD token, and failure to refresh + expiredToken := confidential.AuthResult{AccessToken: "expired_token", ExpiresOn: time.Now().Add(-10 * time.Minute)} + + // Set expectations for mocked functions + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("example.azurecr.io", nil) + mockAADAccessTokenGetter.On("GetAADAccessToken", mock.Anything, "tenantID", "clientID", mock.Anything).Return(confidential.AuthResult{}, errors.New("token refresh failed")) + + // Create WIAuthProvider with expired token + provider := WIAuthProvider{ + aadToken: expiredToken, + tenantID: "tenantID", + clientID: "clientID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getAADAccessToken: mockAADAccessTokenGetter, + reportMetrics: mockMetricsReporter, + } + + // Call Provide method + ctx := context.Background() + _, err := provider.Provide(ctx, "artifact_name") + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "could not refresh AAD token") +} + +// Test when tenant ID is missing from the environment +func TestAzureWIProviderFactory_Create_NoTenantID(t *testing.T) { + // Clear the tenant ID environment variable + t.Setenv("AZURE_TENANT_ID", "") + + // Initialize provider factory + factory := &AzureWIProviderFactory{} + + // Call Create with minimal configuration + _, err := factory.Create(map[string]interface{}{}) + + // Expect error related to missing tenant ID + assert.Error(t, err) + assert.Contains(t, err.Error(), "azure tenant id environment variable is empty") +} + +// Test when client ID is missing from the environment +func TestAzureWIProviderFactory_Create_NoClientID(t *testing.T) { + // Set tenant ID but leave client ID empty + t.Setenv("AZURE_TENANT_ID", "tenantID") + t.Setenv("AZURE_CLIENT_ID", "") + + // Initialize provider factory + factory := &AzureWIProviderFactory{} + + // Call Create with minimal configuration + _, err := factory.Create(map[string]interface{}{}) + + // Expect error related to missing client ID + assert.Error(t, err) + assert.Contains(t, err.Error(), "no client ID provided and AZURE_CLIENT_ID environment variable is empty") +} + +// Test for successful token refresh +func TestWIAuthProvider_Provide_TokenRefresh_Success(t *testing.T) { + // Mock dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockAADAccessTokenGetter := new(MockAADAccessTokenGetter) + mockMetricsReporter := new(MockMetricsReporter) + mockAuthClient := new(MockAuthClient) + + // Mock expired AAD token and refreshed token + expiredToken := confidential.AuthResult{AccessToken: "expired_token", ExpiresOn: time.Now().Add(-10 * time.Minute)} + refreshTokenString := "refreshed_token" + newToken := confidential.AuthResult{AccessToken: "new_token", ExpiresOn: time.Now().Add(10 * time.Minute)} + refreshToken := azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{ + ACRRefreshToken: azcontainerregistry.ACRRefreshToken{RefreshToken: &refreshTokenString}, + } + + // Set expectations + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("example.azurecr.io", nil) + mockAuthClientFactory.On("CreateAuthClient", "https://example.azurecr.io", mock.Anything).Return(mockAuthClient, nil) + mockAuthClient.On("ExchangeAADAccessTokenForACRRefreshToken", mock.Anything, azcontainerregistry.PostContentSchemaGrantType(GrantTypeAccessToken), "example.azurecr.io", mock.Anything).Return(refreshToken, nil) + mockAADAccessTokenGetter.On("GetAADAccessToken", mock.Anything, "tenantID", "clientID", mock.Anything).Return(newToken, nil) + mockMetricsReporter.On("ReportMetrics", mock.Anything, mock.Anything, "example.azurecr.io").Return() + + // Create WIAuthProvider with expired token + provider := WIAuthProvider{ + aadToken: expiredToken, + tenantID: "tenantID", + clientID: "clientID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getAADAccessToken: mockAADAccessTokenGetter, + reportMetrics: mockMetricsReporter, + } + + // Call Provide method + ctx := context.Background() + authConfig, err := provider.Provide(ctx, "artifact_name") + + // Assertions + assert.NoError(t, err) + assert.Equal(t, "refreshed_token", authConfig.Password) +} + +// Test when token refresh fails +func TestWIAuthProvider_Provide_TokenRefreshFailure(t *testing.T) { + // Mock dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockAADAccessTokenGetter := new(MockAADAccessTokenGetter) + mockMetricsReporter := new(MockMetricsReporter) + + // Mock expired AAD token and failure to refresh + expiredToken := confidential.AuthResult{AccessToken: "expired_token", ExpiresOn: time.Now().Add(-10 * time.Minute)} + + // Set expectations + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("example.azurecr.io", nil) + mockAADAccessTokenGetter.On("GetAADAccessToken", mock.Anything, "tenantID", "clientID", mock.Anything).Return(confidential.AuthResult{}, errors.New("token refresh failed")) + + // Create WIAuthProvider with expired token + provider := WIAuthProvider{ + aadToken: expiredToken, + tenantID: "tenantID", + clientID: "clientID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getAADAccessToken: mockAADAccessTokenGetter, + reportMetrics: mockMetricsReporter, + } + + // Call Provide method + ctx := context.Background() + _, err := provider.Provide(ctx, "artifact_name") + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "could not refresh AAD token") +} + +// Test for handling empty AccessToken +func TestWIAuthProvider_Enabled_NoAccessToken(t *testing.T) { + // Create a provider with no AccessToken + provider := WIAuthProvider{ + tenantID: "tenantID", + clientID: "clientID", + aadToken: confidential.AuthResult{AccessToken: ""}, + } + + // Assert that provider is not enabled + enabled := provider.Enabled(context.Background()) + assert.False(t, enabled) +} + +// Test for invalid hostname retrieval +func TestWIAuthProvider_Provide_InvalidHostName(t *testing.T) { + // Mock dependencies + mockAuthClientFactory := new(MockAuthClientFactory) + mockRegistryHostGetter := new(MockRegistryHostGetter) + mockAADAccessTokenGetter := new(MockAADAccessTokenGetter) + mockMetricsReporter := new(MockMetricsReporter) + + // Mock valid AAD token + validToken := confidential.AuthResult{AccessToken: "valid_token", ExpiresOn: time.Now().Add(10 * time.Minute)} + + // Set expectations for an invalid hostname + mockRegistryHostGetter.On("GetRegistryHost", "artifact_name").Return("", errors.New("invalid hostname")) + + // Create WIAuthProvider with valid token + provider := WIAuthProvider{ + aadToken: validToken, + tenantID: "tenantID", + clientID: "clientID", + authClientFactory: mockAuthClientFactory, + registryHostGetter: mockRegistryHostGetter, + getAADAccessToken: mockAADAccessTokenGetter, + reportMetrics: mockMetricsReporter, + } + + // Call Provide method + ctx := context.Background() + _, err := provider.Provide(ctx, "artifact_name") + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "HOST_NAME_INVALID") +} + // Verifies that Enabled checks if tenantID is empty or AAD token is empty func TestAzureWIEnabled_ExpectedResults(t *testing.T) { - azAuthProvider := azureWIAuthProvider{ + azAuthProvider := WIAuthProvider{ tenantID: "test_tenant", clientID: "test_client", aadToken: confidential.AuthResult{ diff --git a/pkg/common/oras/authprovider/azure/helper.go b/pkg/common/oras/authprovider/azure/helper.go new file mode 100644 index 000000000..beafa4db0 --- /dev/null +++ b/pkg/common/oras/authprovider/azure/helper.go @@ -0,0 +1,84 @@ +/* +Copyright The Ratify Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + provider "github.com/ratify-project/ratify/pkg/common/oras/authprovider" +) + +const GrantTypeAccessToken = "access_token" + +// AuthClientFactory defines an interface for creating an authentication client. +type AuthClientFactory interface { + CreateAuthClient(serverURL string, options *azcontainerregistry.AuthenticationClientOptions) (AuthClient, error) +} + +// defaultAuthClientFactoryImpl is the default implementation of AuthClientFactory. +type defaultAuthClientFactoryImpl struct{} + +// creates an AuthClient using the default factory implementation. +// Return an AuthClient and an error if the client creation fails. +func (f *defaultAuthClientFactoryImpl) CreateAuthClient(serverURL string, options *azcontainerregistry.AuthenticationClientOptions) (AuthClient, error) { + return defaultAuthClientFactory(serverURL, options) +} + +// Define a helper function that creates an instance of AuthenticationClientWrapper. +func defaultAuthClientFactory(serverURL string, options *azcontainerregistry.AuthenticationClientOptions) (AuthClient, error) { + client, err := azcontainerregistry.NewAuthenticationClient(serverURL, options) + if err != nil { + return nil, err + } + return &AuthenticationClientWrapper{client: client}, nil +} + +// Define the interface for azcontainerregistry.AuthenticationClient methods used +type AuthenticationClientInterface interface { + ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, grantType azcontainerregistry.PostContentSchemaGrantType, service string, options *azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) +} + +// Define the wrapper for AuthenticationClientInterface +type AuthenticationClientWrapper struct { + client AuthenticationClientInterface +} + +// A wrapper method that calls the underlying AuthenticationClientInterface's method. +// Exchanges an AAD access token for an ACR refresh token. +func (w *AuthenticationClientWrapper) ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, grantType azcontainerregistry.PostContentSchemaGrantType, service string, options *azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) { + return w.client.ExchangeAADAccessTokenForACRRefreshToken(ctx, grantType, service, options) +} + +// define the interface for authentication operations. +// It includes the method for exchanging an AAD access token for an ACR refresh token. +type AuthClient interface { + ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, grantType azcontainerregistry.PostContentSchemaGrantType, service string, options *azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) +} + +// RegistryHostGetter defines an interface for getting the registry host. +type RegistryHostGetter interface { + GetRegistryHost(artifact string) (string, error) +} + +// defaultRegistryHostGetterImpl is the default implementation of RegistryHostGetter. +type defaultRegistryHostGetterImpl struct{} + +// Retrieves the registry host name for a given artifact. +// It utilizes the provider's GetRegistryHostName function to perform the lookup. +func (g *defaultRegistryHostGetterImpl) GetRegistryHost(artifact string) (string, error) { + return provider.GetRegistryHostName(artifact) +} diff --git a/pkg/common/oras/authprovider/azure/helper_test.go b/pkg/common/oras/authprovider/azure/helper_test.go new file mode 100644 index 000000000..49c811f0f --- /dev/null +++ b/pkg/common/oras/authprovider/azure/helper_test.go @@ -0,0 +1,100 @@ +/* +Copyright The Ratify Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure + +import ( + "context" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// MockAuthClient is a mock implementation of AuthClient. +type MockAuthClient struct { + mock.Mock +} + +// Mock method for ExchangeAADAccessTokenForACRRefreshToken +func (m *MockAuthClient) ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, grantType azcontainerregistry.PostContentSchemaGrantType, service string, options *azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) { + args := m.Called(ctx, grantType, service, options) + return args.Get(0).(azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse), args.Error(1) +} + +// MockAuthClientFactory is a mock implementation of AuthClientFactory. +type MockAuthClientFactory struct { + mock.Mock +} + +// Mock method for CreateAuthClient +func (m *MockAuthClientFactory) CreateAuthClient(serverURL string, options *azcontainerregistry.AuthenticationClientOptions) (AuthClient, error) { + args := m.Called(serverURL, options) + return args.Get(0).(AuthClient), args.Error(1) +} + +// MockRegistryHostGetter is a mock implementation of RegistryHostGetter. +type MockRegistryHostGetter struct { + mock.Mock +} + +// Mock method for GetRegistryHost +func (m *MockRegistryHostGetter) GetRegistryHost(artifact string) (string, error) { + args := m.Called(artifact) + return args.String(0), args.Error(1) +} + +func TestDefaultAuthClientFactoryImpl_CreateAuthClient(t *testing.T) { + factory := &defaultAuthClientFactoryImpl{} + serverURL := "https://example.com" + options := &azcontainerregistry.AuthenticationClientOptions{} + + client, err := factory.CreateAuthClient(serverURL, options) + assert.Nil(t, err) + assert.NotNil(t, client) +} + +func TestDefaultAuthClientFactory(t *testing.T) { + serverURL := "https://example.com" + options := &azcontainerregistry.AuthenticationClientOptions{} + + client, err := defaultAuthClientFactory(serverURL, options) + assert.Nil(t, err) + assert.NotNil(t, client) +} + +func TestDefaultRegistryHostGetterImpl_GetRegistryHost(t *testing.T) { + getter := &defaultRegistryHostGetterImpl{} + artifact := "example.azurecr.io/myArtifact" + + host, err := getter.GetRegistryHost(artifact) + assert.Nil(t, err) + assert.Equal(t, "example.azurecr.io", host) +} + +func TestAuthenticationClientWrapper_ExchangeAADAccessTokenForACRRefreshToken(t *testing.T) { + mockClient := new(MockAuthClient) + wrapper := &AuthenticationClientWrapper{client: mockClient} + ctx := context.Background() + grantType := azcontainerregistry.PostContentSchemaGrantType("grantType") + service := "service" + options := &azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{} + + mockClient.On("ExchangeAADAccessTokenForACRRefreshToken", ctx, grantType, service, options).Return(azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{}, nil) + + _, err := wrapper.ExchangeAADAccessTokenForACRRefreshToken(ctx, grantType, service, options) + assert.Nil(t, err) +}