diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 38c6a62739d..45f60548dbe 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,9 +33,9 @@ jobs: contents: read steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - - uses: sigstore/cosign-installer@d6a3abf1bdea83574e28d40543793018b6035605 # v2.0.1 + - uses: sigstore/cosign-installer@bb61838e7ee5bf314f85f2e219b3706835fa6306 # v2.0.1 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: @@ -48,7 +48,7 @@ jobs: run: go get golang.org/x/tools/cmd/goimports - name: Set up Cloud SDK - uses: google-github-actions/auth@50dbfd0907520dcccbd51e965728eb32e592b8fa # v0.6.0 + uses: google-github-actions/auth@b258a9f230b36c9fa86dfaa43d1906bd76399edb # v0.6.0 with: workload_identity_provider: 'projects/498091336538/locations/global/workloadIdentityPools/githubactions/providers/sigstore-cosign' service_account: 'github-actions@projectsigstore.iam.gserviceaccount.com' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a759e2eef77..b7fcebd959f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - name: Utilize Go Module Cache uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 diff --git a/.github/workflows/cross.yaml b/.github/workflows/cross.yaml index 8f7e0acd411..e501b45ec3a 100644 --- a/.github/workflows/cross.yaml +++ b/.github/workflows/cross.yaml @@ -33,7 +33,7 @@ jobs: with: go-version: '1.17.x' - name: Checkout code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - name: build cosign run: | make cosign && mv ./cosign ./${{matrix.COSIGN_TARGET}} diff --git a/.github/workflows/donotsubmit.yaml b/.github/workflows/donotsubmit.yaml index 5a046551509..c92b095dd29 100644 --- a/.github/workflows/donotsubmit.yaml +++ b/.github/workflows/donotsubmit.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 #v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v2.4.0 - name: Do Not Submit uses: chainguard-dev/actions/donotsubmit@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/.github/workflows/e2e-with-binary.yml b/.github/workflows/e2e-with-binary.yml index 95b02ff9561..4c1a81ab22c 100644 --- a/.github/workflows/e2e-with-binary.yml +++ b/.github/workflows/e2e-with-binary.yml @@ -38,7 +38,7 @@ jobs: COSIGN_EXPERIMENTAL: "true" steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index ee407d8fa26..d3ab0597bd3 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -31,7 +31,7 @@ jobs: os: [macos-latest, ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/github-oidc.yaml b/.github/workflows/github-oidc.yaml index d6820f155a1..8d62acda63a 100644 --- a/.github/workflows/github-oidc.yaml +++ b/.github/workflows/github-oidc.yaml @@ -35,13 +35,13 @@ jobs: KO_PREFIX: ghcr.io/${{ github.repository }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' # Install tools. - - uses: sigstore/cosign-installer@d6a3abf1bdea83574e28d40543793018b6035605 # v2.0.1 + - uses: sigstore/cosign-installer@bb61838e7ee5bf314f85f2e219b3706835fa6306 # v2.0.1 - uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Build and sign a container image diff --git a/.github/workflows/kind-cluster-image-policy-with-attestations.yaml b/.github/workflows/kind-cluster-image-policy-with-attestations.yaml new file mode 100644 index 00000000000..c52d490d592 --- /dev/null +++ b/.github/workflows/kind-cluster-image-policy-with-attestations.yaml @@ -0,0 +1,97 @@ +# Copyright 2022 The Sigstore 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. + +name: Test cosigned with ClusterImagePolicy with attestations + +on: + pull_request: + branches: [ 'main', 'release-*' ] + +defaults: + run: + shell: bash + +permissions: read-all + +jobs: + cip-test: + name: ClusterImagePolicy e2e tests + runs-on: ubuntu-latest + + strategy: + matrix: + k8s-version: + - v1.21.x + - v1.22.x + # Try without this one now, might have problems with job restartings + # may require upstream changes. + - v1.23.x + + env: + KNATIVE_VERSION: "1.1.0" + KO_DOCKER_REPO: "registry.local:5000/cosigned" + SCAFFOLDING_RELEASE_VERSION: "v0.2.8" + GO111MODULE: on + GOFLAGS: -ldflags=-s -ldflags=-w + KOCACHE: ~/ko + COSIGN_EXPERIMENTAL: true + + steps: + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 + with: + go-version: '1.17.x' + + # will use the latest release available for ko + - uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 + + - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 + + - name: Install yq + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 + + - name: Setup mirror + uses: chainguard-dev/actions/setup-mirror@main + with: + mirror: mirror.gcr.io + + - name: build cosign + run: | + make cosign + + - name: Install cluster + cosign + uses: sigstore/scaffolding/actions/setup@main + + - name: Install cosigned + env: + GIT_HASH: ${{ github.sha }} + GIT_VERSION: ci + LDFLAGS: "" + COSIGNED_YAML: cosigned-e2e.yaml + KO_PREFIX: registry.local:5000/cosigned + COSIGNED_ARCHS: linux/amd64 + run: | + make ko-cosigned + kubectl apply -f cosigned-e2e.yaml + + # Wait for the webhook to come up and become Ready + kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook + + - name: Run Cluster Image Policy Tests with attestations + run: | + ./test/e2e_test_cluster_image_policy_with_attestations.sh + + - name: Collect diagnostics + if: ${{ failure() }} + uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index f63e353677d..cacedf1c2c3 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -48,7 +48,7 @@ jobs: COSIGN_EXPERIMENTAL: true steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' @@ -59,7 +59,7 @@ jobs: - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 - name: Install yq - uses: mikefarah/yq@bc2118736bca883de2e2c345bb7f7ef52c994920 # v4.16.2 + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 - name: Setup mirror uses: chainguard-dev/actions/setup-mirror@main @@ -88,123 +88,9 @@ jobs: # Wait for the webhook to come up and become Ready kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook - - name: Create sample image - demoimage + - name: Run Cluster Image Policy Tests run: | - pushd $(mktemp -d) - go mod init example.com/demo - cat < main.go - package main - import "fmt" - func main() { - fmt.Println("hello world") - } - EOF - demoimage=`ko publish -B example.com/demo` - echo "demoimage=$demoimage" >> $GITHUB_ENV - echo Created image $demoimage - popd - - - name: Create sample image2 - demoimage2 - run: | - pushd $(mktemp -d) - go mod init example.com/demo2 - cat < main.go - package main - import "fmt" - func main() { - fmt.Println("hello world 2") - } - EOF - demoimage2=`ko publish -B example.com/demo2` - echo "demoimage2=$demoimage2" >> $GITHUB_ENV - echo Created image $demoimage2 - popd - - - name: Deploy ClusterImagePolicy With Keyless Signing - run: | - kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless.yaml - - - name: Sign demoimage with cosign - run: | - ./cosign sign --rekor-url ${{ env.REKOR_URL }} --fulcio-url ${{ env.FULCIO_URL }} --force --allow-insecure-registry ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} - - - name: Verify with cosign - run: | - SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} - - - name: Deploy jobs and verify signed works, unsigned fails - run: | - kubectl create namespace demo-keyless-signing - kubectl label namespace demo-keyless-signing cosigned.sigstore.dev/include=true - - echo '::group:: test job success' - # We signed this above, this should work - if ! kubectl create -n demo-keyless-signing job demo --image=${{ env.demoimage }} ; then - echo Failed to create Job in namespace without label! - exit 1 - else - echo Succcessfully created Job with signed image - fi - echo '::endgroup:: test job success' - - echo '::group:: test job rejection' - # We did not sign this, should fail - if kubectl create -n demo-keyless-signing job demo2 --image=${{ env.demoimage2 }} ; then - echo Failed to block unsigned Job creation! - exit 1 - else - echo Successfully blocked Job creation with unsigned image - fi - echo '::endgroup::' - - - name: Generate New Signing Key - run: | - COSIGN_PASSWORD="" ./cosign generate-key-pair - - - name: Deploy ClusterImagePolicy With Key Signing - run: | - yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key.yaml | \ - kubectl apply -f - - - - name: Verify with two CIP, one not signed with public key - run: | - if kubectl create -n demo-key-signing job demo --image=${{ env.demoimage }}; then - echo Failed to block unsigned Job creation! - exit 1 - fi - - - name: Sign demoimage with cosign key - run: | - ./cosign sign --key cosign.key --force --allow-insecure-registry ${{ env.demoimage }} - - - name: Verify with cosign - run: | - ./cosign verify --key cosign.pub --allow-insecure-registry ${{ env.demoimage }} - - - name: Deploy jobs and verify signed works, unsigned fails - run: | - kubectl create namespace demo-key-signing - kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true - - echo '::group:: test job success' - # We signed this above, this should work - if ! kubectl create -n demo-key-signing job demo --image=${{ env.demoimage }} ; then - echo Failed to create Job in namespace without label! - exit 1 - else - echo Succcessfully created Job with signed image - fi - echo '::endgroup:: test job success' - - echo '::group:: test job rejection' - # We did not sign this, should fail - if kubectl create -n demo-key-signing job demo2 --image=${{ env.demoimage2 }} ; then - echo Failed to block unsigned Job creation! - exit 1 - else - echo Successfully blocked Job creation with unsigned image - fi - echo '::endgroup::' + ./test/e2e_test_cluster_image_policy.sh - name: Collect diagnostics if: ${{ failure() }} diff --git a/.github/workflows/kind-e2e-cosigned.yaml b/.github/workflows/kind-e2e-cosigned.yaml index 32295f0c06e..555a2e9f7b6 100644 --- a/.github/workflows/kind-e2e-cosigned.yaml +++ b/.github/workflows/kind-e2e-cosigned.yaml @@ -43,7 +43,7 @@ jobs: KO_DOCKER_REPO: registry.local:5000/cosigned steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: 1.17.x @@ -53,7 +53,7 @@ jobs: - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 - name: Install yq - uses: mikefarah/yq@bc2118736bca883de2e2c345bb7f7ef52c994920 # v4.16.2 + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 - name: Install Cosign run: | diff --git a/.github/workflows/kind-verify-attestation.yaml b/.github/workflows/kind-verify-attestation.yaml index 553313d04ce..a72e8a71fe6 100644 --- a/.github/workflows/kind-verify-attestation.yaml +++ b/.github/workflows/kind-verify-attestation.yaml @@ -45,10 +45,13 @@ jobs: GO111MODULE: on GOFLAGS: -ldflags=-s -ldflags=-w KOCACHE: ~/ko - COSIGN_EXPERIMENTAL: true + # Trust the custom Rekor API endpoint for fetching the Public Key from it. + SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY: "true" + # We are only testing keyless here, so set it. + COSIGN_EXPERIMENTAL: "true" steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' @@ -57,7 +60,7 @@ jobs: - uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Install yq - uses: mikefarah/yq@bc2118736bca883de2e2c345bb7f7ef52c994920 # v4.16.2 + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 - name: build cosign run: | @@ -89,28 +92,28 @@ jobs: - name: Create attestation for it run: | echo -n 'foobar e2e test' > ./predicate-file - SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign attest --predicate ./predicate-file --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} + ./cosign attest --predicate ./predicate-file --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} - name: Verify with cosign run: | - SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} + ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} - - name: Verify attestation with cosign, works + - name: Verify custom attestation with cosign, works run: | - echo '::group:: test verify-attestation success' - if ! SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --policy ./test/testdata/policies/cue-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo '::group:: test custom verify-attestation success' + if ! ./cosign verify-attestation --policy ./test/testdata/policies/cue-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then echo Failed to verify attestation with a valid policy exit 1 else - echo Successfully validated attestation with a valid policy + echo Successfully validated custom attestation with a valid policy fi echo '::endgroup::' - - name: Verify attestation with cosign, fails + - name: Verify custom attestation with cosign, fails run: | - echo '::group:: test verify-attestation success' - if SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --policy ./test/testdata/policies/cue-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then - echo verify-attestation succeeded with cue policy that should not work + echo '::group:: test custom verify-attestation success' + if ./cosign verify-attestation --policy ./test/testdata/policies/cue-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo custom verify-attestation succeeded with cue policy that should not work exit 1 else echo Successfully failed a policy that should not work @@ -120,3 +123,29 @@ jobs: - name: Collect diagnostics if: ${{ failure() }} uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main + + - name: Create vuln attestation for it + run: | + ./cosign attest --predicate ./test/testdata/attestations/vuln-predicate.json --type vuln --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} + + - name: Verify vuln attestation with cosign, works + run: | + echo '::group:: test vuln verify-attestation success' + if ! ./cosign verify-attestation --type vuln --policy ./test/testdata/policies/cue-vuln-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo Failed to verify attestation with a valid policy + exit 1 + else + echo Successfully validated vuln attestation with a valid policy + fi + echo '::endgroup::' + + - name: Verify vuln attestation with cosign, fails + run: | + echo '::group:: test vuln verify-attestation success' + if ./cosign verify-attestation --type vuln --policy ./test/testdata/policies/cue-vuln-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo verify-attestation succeeded with cue policy that should not work + exit 1 + else + echo Successfully failed a policy that should not work + fi + echo '::endgroup::' diff --git a/.github/workflows/scorecard_action.yml b/.github/workflows/scorecard_action.yml index 90a76bc0fe8..897ce5ea8a7 100644 --- a/.github/workflows/scorecard_action.yml +++ b/.github/workflows/scorecard_action.yml @@ -23,7 +23,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 with: persist-credentials: false diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml index e57b1b27ab1..349f58c3ce7 100644 --- a/.github/workflows/style.yaml +++ b/.github/workflows/style.yaml @@ -18,7 +18,7 @@ jobs: go-version: 1.16.x - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: chainguard-dev/actions/gofmt@84c993eaf02da1c325854fb272a4df9184bd80fc # main with: @@ -35,6 +35,6 @@ jobs: go-version: 1.16.x - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: chainguard-dev/actions/goimports@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d4bc6daf8d2..4dafc04aa55 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,7 +38,7 @@ jobs: OS: ${{ matrix.os }} steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds - uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 with: @@ -61,7 +61,7 @@ jobs: - name: Run Go tests run: go test -covermode atomic -coverprofile coverage.txt $(go list ./... | grep -v third_party/) - name: Upload Coverage Report - uses: codecov/codecov-action@e3c560433a6cc60aec8812599b7844a7b4fa0d71 # v2.1.0 + uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v2.1.0 with: env_vars: OS - name: Run Go tests w/ `-race` @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds - uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 with: @@ -111,7 +111,7 @@ jobs: name: Run PowerShell E2E tests runs-on: windows-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.1.5 with: go-version: ${{ env.GO_VERSION }} @@ -136,7 +136,7 @@ jobs: name: license boilerplate check runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: ${{ env.GO_VERSION }} @@ -151,7 +151,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index e5077a03f42..7bb34d48668 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -39,11 +39,11 @@ jobs: statuses: none env: - CROSS_BUILDER_IMAGE: ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3 - COSIGN_IMAGE: gcr.io/projectsigstore/cosign:v1.6.0@sha256:b667002156c4bf9fedd9273f689b800bb5c341660e710e3bbac981c9795423d9 + CROSS_BUILDER_IMAGE: ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418 + COSIGN_IMAGE: gcr.io/projectsigstore/cosign:v1.7.2@sha256:ad2985a87622d5934a4bc06a61faadff772e377937e42519af4f506e1b019d1e steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 #v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v2.4.0 - name: Check Signature run: | diff --git a/.github/workflows/verify-codegen.yaml b/.github/workflows/verify-codegen.yaml index dcb1b65b796..f9708711763 100644 --- a/.github/workflows/verify-codegen.yaml +++ b/.github/workflows/verify-codegen.yaml @@ -36,7 +36,7 @@ jobs: with: go-version: 1.17.x - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b with: path: ./src/github.com/${{ github.repository }} fetch-depth: 0 diff --git a/.github/workflows/verify-docgen.yaml b/.github/workflows/verify-docgen.yaml index c9aed73023d..a247277967c 100644 --- a/.github/workflows/verify-docgen.yaml +++ b/.github/workflows/verify-docgen.yaml @@ -31,7 +31,7 @@ jobs: steps: - name: deps run: sudo apt-get update && sudo apt-get install -yq libpcsclite-dev - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/whitespace.yaml b/.github/workflows/whitespace.yaml index 5d3589fe5c8..4c9833de92d 100644 --- a/.github/workflows/whitespace.yaml +++ b/.github/workflows/whitespace.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 #v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v2.4.0 - uses: chainguard-dev/actions/trailing-space@84c993eaf02da1c325854fb272a4df9184bd80fc # main if: ${{ always() }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 95351e05709..b738aa652a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,77 @@ +# v1.8.0 + +_NOTE_: If you use Fulcio to issue certificates you will need to use this release. + +## Enhancements + +* Handle context cancelled properly + tests. (https://github.com/sigstore/cosign/pull/1796) +* Allow passing keys via environment variables (`env://` refs) (https://github.com/sigstore/cosign/pull/1794) +* Add parallelization for processing policies / authorities. (https://github.com/sigstore/cosign/pull/1795) +* Attestations + policy in cip. (https://github.com/sigstore/cosign/pull/1772) +* Refactor fulcio signer to take in KeyOpts. (https://github.com/sigstore/cosign/pull/1788) +* Remove the dependency on v1alpha1.Identity which brings in (https://github.com/sigstore/cosign/pull/1790) +* Add Fulcio intermediate CA certificate to intermediate pool (https://github.com/sigstore/cosign/pull/1774) +* Cosigned validate against remote sig src (https://github.com/sigstore/cosign/pull/1754) +* tuf: add debug info if tuf update fails (https://github.com/sigstore/cosign/pull/1766) +* Break the CIP action tests into a sh script. (https://github.com/sigstore/cosign/pull/1767) +* [policy-webhook] The webhooks name is now configurable via --(validating|mutating)-webhook-name flags (https://github.com/sigstore/cosign/pull/1757) +* Verify embedded SCTs (https://github.com/sigstore/cosign/pull/1731) +* Validate issuer/subject regexp in validate webhook. (https://github.com/sigstore/cosign/pull/1761) +* Add intermediate CA certificate pool for Fulcio (https://github.com/sigstore/cosign/pull/1749) +* [cosigned] The webhook name is now configurable via --webhook-name flag (https://github.com/sigstore/cosign/pull/1726) +* Use bundle log ID to find verification key (https://github.com/sigstore/cosign/pull/1748) +* Refactor policy related code, add support for vuln verify (https://github.com/sigstore/cosign/pull/1747) +* Create convert functions for internal CIP (https://github.com/sigstore/cosign/pull/1736) +* Move the KMS integration imports into the binary entrypoints (https://github.com/sigstore/cosign/pull/1744) + +## Bug Fixes + +* Fix a bug where an error would send duplicate results. (https://github.com/sigstore/cosign/pull/1797) +* fix: more informative error (https://github.com/sigstore/cosign/pull/1778) +* fix: add support for rsa keys (https://github.com/sigstore/cosign/pull/1768) +* Implement identities, fix bug in webhook validation. (https://github.com/sigstore/cosign/pull/1759) + +## Others + +* Bump github.com/hashicorp/go-retryablehttp from 0.7.0 to 0.7.1 (https://github.com/sigstore/cosign/pull/1758) +* Bump google-github-actions/auth from 0.7.0 to 0.7.1 (https://github.com/sigstore/cosign/pull/1801) +* Bump google.golang.org/grpc from 1.45.0 to 1.46.0 (https://github.com/sigstore/cosign/pull/1800) +* Bump github.com/xanzy/go-gitlab from 0.63.0 to 0.64.0 (https://github.com/sigstore/cosign/pull/1799) +* Revert "Refactor fulcio signer to take in KeyOpts. (https://github.com/sigstore/cosign/pull/1788)" (https://github.com/sigstore/cosign/pull/1798) +* chore: add rego function to consume modules (https://github.com/sigstore/cosign/pull/1787) +* test: add cue unit tests (https://github.com/sigstore/cosign/pull/1791) +* Run update-codegen. (https://github.com/sigstore/cosign/pull/1789) +* Bump actions/checkout from 3.0.1 to 3.0.2 (https://github.com/sigstore/cosign/pull/1783) +* Bump github.com/mitchellh/mapstructure from 1.4.3 to 1.5.0 (https://github.com/sigstore/cosign/pull/1782) +* Bump k8s.io/code-generator from 0.23.5 to 0.23.6 (https://github.com/sigstore/cosign/pull/1781) +* Bump google.golang.org/api from 0.74.0 to 0.75.0 (https://github.com/sigstore/cosign/pull/1780) +* Bump cuelang.org/go from 0.4.2 to 0.4.3 (https://github.com/sigstore/cosign/pull/1779) +* Bump codecov/codecov-action from 3.0.0 to 3.1.0 (https://github.com/sigstore/cosign/pull/1784) +* Bump actions/checkout from 3.0.0 to 3.0.1 (https://github.com/sigstore/cosign/pull/1764) +* Bump mikefarah/yq from 4.24.4 to 4.24.5 (https://github.com/sigstore/cosign/pull/1765) +* chore: add warning when downloading a sBOM (https://github.com/sigstore/cosign/pull/1763) +* chore: add warn when attaching sBOM (https://github.com/sigstore/cosign/pull/1756) +* Bump sigstore/cosign-installer from 2.2.0 to 2.2.1 (https://github.com/sigstore/cosign/pull/1752) +* update go builder and cosign images (https://github.com/sigstore/cosign/pull/1755) +* test: create fake TUF test root and create test SETs for verification (https://github.com/sigstore/cosign/pull/1750) +* Bump github.com/spf13/viper from 1.10.1 to 1.11.0 (https://github.com/sigstore/cosign/pull/1751) +* Bump mikefarah/yq from 4.24.2 to 4.24.4 (https://github.com/sigstore/cosign/pull/1746) +* Bump github.com/xanzy/go-gitlab from 0.62.0 to 0.63.0 (https://github.com/sigstore/cosign/pull/1745) + +## Contributors + +* Asra Ali (@asraa) +* Billy Lynch (@wlynch) +* Carlos Tadeu Panato Junior (@cpanato) +* Denny (@DennyHoang) +* Hayden Blauzvern (@haydentherapper) +* Hector Fernandez (@hectorj2f) +* Matt Moore (@mattmoor) +* Ville Aikas (@vaikas) +* Vladimir Nachev (@vpnachev) +* Youssef Bel Mekki (@ybelMekk) +* Zack Newman (@znewman01) + # v1.7.2 ## Bug Fixes diff --git a/cmd/cosign/cli/attach.go b/cmd/cosign/cli/attach.go index fbea7b0dc85..99281af7a61 100644 --- a/cmd/cosign/cli/attach.go +++ b/cmd/cosign/cli/attach.go @@ -16,6 +16,9 @@ package cli import ( + "fmt" + "os" + "github.com/sigstore/cosign/cmd/cosign/cli/attach" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/spf13/cobra" @@ -67,6 +70,7 @@ func attachSBOM() *cobra.Command { if err != nil { return err } + fmt.Fprintf(os.Stderr, "WARNING: Attaching SBOMs this way does not sign them. If you want to sign them, use 'cosign attest -predicate %s -key ' or 'cosign sign -key '.\n", o.SBOM) return attach.SBOMCmd(cmd.Context(), o.Registry, o.SBOM, mediaType, args[0]) }, } diff --git a/cmd/cosign/cli/attach/attestation.go b/cmd/cosign/cli/attach/attestation.go index 47255aa966c..e6f35c7527c 100644 --- a/cmd/cosign/cli/attach/attestation.go +++ b/cmd/cosign/cli/attach/attestation.go @@ -42,6 +42,10 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, signed return err } + if len(payload) == 0 { + return fmt.Errorf("%s payload is empty", signedPayload) + } + env := ssldsse.Envelope{} if err := json.Unmarshal(payload, &env); err != nil { return err diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index fdcf524c49e..27e37b2b778 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -92,6 +92,7 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/download.go b/cmd/cosign/cli/download.go index ce2410bf848..cdd5fcf99b7 100644 --- a/cmd/cosign/cli/download.go +++ b/cmd/cosign/cli/download.go @@ -16,6 +16,9 @@ package cli import ( + "fmt" + "os" + "github.com/spf13/cobra" "github.com/sigstore/cosign/cmd/cosign/cli/download" @@ -64,6 +67,7 @@ func downloadSBOM() *cobra.Command { Example: " cosign download sbom ", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(os.Stderr, "WARNING: Downloading SBOMs this way does not ensure its authenticity. If you want to ensure a tamper-proof SBOM, download it using 'cosign download attestation ' or verify its signature.") _, err := download.SBOMCmd(cmd.Context(), *o, args[0], cmd.OutOrStdout()) return err }, diff --git a/cmd/cosign/cli/fulcio/depcheck_test.go b/cmd/cosign/cli/fulcio/depcheck_test.go index 39493e5a86c..bc7d45f2f26 100644 --- a/cmd/cosign/cli/fulcio/depcheck_test.go +++ b/cmd/cosign/cli/fulcio/depcheck_test.go @@ -25,7 +25,6 @@ func TestNoDeps(t *testing.T) { depcheck.AssertNoDependency(t, map[string][]string{ "github.com/sigstore/cosign/cmd/cosign/cli/fulcio": { // Avoid pulling in a variety of things that are massive dependencies. - "github.com/google/certificate-transparency-go", "github.com/google/trillian", "github.com/envoyproxy/go-control-plane", "github.com/gogo/protobuf/protoc-gen-gogo", diff --git a/cmd/cosign/cli/fulcio/fulcio.go b/cmd/cosign/cli/fulcio/fulcio.go index 338cdd4d1ed..d7eedabafb8 100644 --- a/cmd/cosign/cli/fulcio/fulcio.go +++ b/cmd/cosign/cli/fulcio/fulcio.go @@ -157,6 +157,10 @@ func GetRoots() *x509.CertPool { return fulcioroots.Get() } +func GetIntermediates() *x509.CertPool { + return fulcioroots.GetIntermediates() +} + func NewClient(fulcioURL string) (api.Client, error) { fulcioServer, err := url.Parse(fulcioURL) if err != nil { diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go index 0485721b379..c0890bd77c2 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go @@ -16,6 +16,7 @@ package fulcioroots import ( + "bytes" "context" "crypto/x509" "os" @@ -23,11 +24,13 @@ import ( "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/sigstore/pkg/cryptoutils" ) var ( - rootsOnce sync.Once - roots *x509.CertPool + rootsOnce sync.Once + roots *x509.CertPool + intermediates *x509.CertPool ) // This is the root in the fulcio project. @@ -36,6 +39,23 @@ var fulcioTargetStr = `fulcio.crt.pem` // This is the v1 migrated root. var fulcioV1TargetStr = `fulcio_v1.crt.pem` +// The untrusted intermediate CA certificate, used for chain building +// TODO: Remove once this is bundled in TUF metadata. +var fulcioIntermediateV1 = `-----BEGIN CERTIFICATE----- +MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 +7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS +0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB +BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp +KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI +zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR +nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP +mygUY7Ii2zbdCdliiow= +-----END CERTIFICATE-----` + const ( altRoot = "SIGSTORE_ROOT_FILE" ) @@ -43,7 +63,7 @@ const ( func Get() *x509.CertPool { rootsOnce.Do(func() { var err error - roots, err = initRoots() + roots, intermediates, err = initRoots() if err != nil { panic(err) } @@ -51,37 +71,84 @@ func Get() *x509.CertPool { return roots } -func initRoots() (*x509.CertPool, error) { - cp := x509.NewCertPool() +func GetIntermediates() *x509.CertPool { + rootsOnce.Do(func() { + var err error + roots, intermediates, err = initRoots() + if err != nil { + panic(err) + } + }) + return intermediates +} + +func initRoots() (*x509.CertPool, *x509.CertPool, error) { + var rootPool *x509.CertPool + var intermediatePool *x509.CertPool + rootEnv := os.Getenv(altRoot) if rootEnv != "" { raw, err := os.ReadFile(rootEnv) if err != nil { - return nil, errors.Wrap(err, "error reading root PEM file") + return nil, nil, errors.Wrap(err, "error reading root PEM file") } - if !cp.AppendCertsFromPEM(raw) { - return nil, errors.New("error creating root cert pool") + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(raw) + if err != nil { + return nil, nil, errors.Wrap(err, "error unmarshalling certificates") + } + for _, cert := range certs { + // root certificates are self-signed + if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + if rootPool == nil { + rootPool = x509.NewCertPool() + } + rootPool.AddCert(cert) + } else { + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } + intermediatePool.AddCert(cert) + } } } else { tufClient, err := tuf.NewFromEnv(context.Background()) if err != nil { - return nil, errors.Wrap(err, "initializing tuf") + return nil, nil, errors.Wrap(err, "initializing tuf") } defer tufClient.Close() // Retrieve from the embedded or cached TUF root. If expired, a network // call is made to update the root. targets, err := tufClient.GetTargetsByMeta(tuf.Fulcio, []string{fulcioTargetStr, fulcioV1TargetStr}) if err != nil { - return nil, errors.New("error getting targets") + return nil, nil, errors.New("error getting targets") } if len(targets) == 0 { - return nil, errors.New("none of the Fulcio roots have been found") + return nil, nil, errors.New("none of the Fulcio roots have been found") } for _, t := range targets { - if !cp.AppendCertsFromPEM(t.Target) { - return nil, errors.New("error creating root cert pool") + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(t.Target) + if err != nil { + return nil, nil, errors.Wrap(err, "error unmarshalling certificates") + } + for _, cert := range certs { + // root certificates are self-signed + if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + if rootPool == nil { + rootPool = x509.NewCertPool() + } + rootPool.AddCert(cert) + } else { + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } + intermediatePool.AddCert(cert) + } } } + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } + intermediatePool.AppendCertsFromPEM([]byte(fulcioIntermediateV1)) } - return cp, nil + return rootPool, intermediatePool, nil } diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go index fba782e2d45..b400f453a82 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go @@ -23,23 +23,34 @@ import ( ) func TestGetFulcioRoots(t *testing.T) { - rootCert, _, _ := test.GenerateRootCa() - pemCert, _ := cryptoutils.MarshalCertificateToPEM(rootCert) + rootCert, rootPriv, _ := test.GenerateRootCa() + rootPemCert, _ := cryptoutils.MarshalCertificateToPEM(rootCert) + subCert, _, _ := test.GenerateSubordinateCa(rootCert, rootPriv) + subPemCert, _ := cryptoutils.MarshalCertificateToPEM(subCert) + + var chain []byte + chain = append(chain, subPemCert...) + chain = append(chain, rootPemCert...) tmpCertFile, err := os.CreateTemp(t.TempDir(), "cosign_fulcio_root_*.cert") if err != nil { t.Fatalf("failed to create temp cert file: %v", err) } defer tmpCertFile.Close() - if _, err := tmpCertFile.Write(pemCert); err != nil { + if _, err := tmpCertFile.Write(chain); err != nil { t.Fatalf("failed to write cert file: %v", err) } - os.Setenv("SIGSTORE_ROOT_FILE", tmpCertFile.Name()) - defer os.Unsetenv("SIGSTORE_ROOT_FILE") + t.Setenv("SIGSTORE_ROOT_FILE", tmpCertFile.Name()) + + rootCertPool := Get() + // ignore deprecation error because certificates do not contain from SystemCertPool + if len(rootCertPool.Subjects()) != 1 { // nolint:staticcheck + t.Errorf("expected 1 root certificate, got 0") + } - certPool := Get() + subCertPool := GetIntermediates() // ignore deprecation error because certificates do not contain from SystemCertPool - if len(certPool.Subjects()) == 0 { // nolint:staticcheck - t.Errorf("expected 1 or more certificates, got 0") + if len(subCertPool.Subjects()) != 1 { // nolint:staticcheck + t.Errorf("expected 1 intermediate certificate, got 0") } } diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/garbage-there-are-limits b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/garbage-there-are-limits similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/garbage-there-are-limits rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/garbage-there-are-limits diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/google b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/google similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/google rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/google diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/letsencrypt-testflume-2021 b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/letsencrypt-testflume-2021 similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/letsencrypt-testflume-2021 rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/letsencrypt-testflume-2021 diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/rsa b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/rsa similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/rsa rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/rsa diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go new file mode 100644 index 00000000000..c2af470cc21 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go @@ -0,0 +1,224 @@ +// Copyright 2022 The Sigstore 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 ctl + +import ( + "context" + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "os" + + ct "github.com/google/certificate-transparency-go" + ctx509 "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" + "github.com/pkg/errors" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctutil" + + "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +// This is the CT log public key target name +var ctPublicKeyStr = `ctfe.pub` + +// Setting this env variable will over ride what is used to validate +// the SCT coming back from Fulcio. +const altCTLogPublicKeyLocation = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" + +// logIDMetadata holds information for mapping a key ID hash (log ID) to associated data. +type logIDMetadata struct { + pubKey crypto.PublicKey + status tuf.StatusKind +} + +// ContainsSCT checks if the certificate contains embedded SCTs. cert can either be +// DER or PEM encoded. +func ContainsSCT(cert []byte) (bool, error) { + embeddedSCTs, err := x509util.ParseSCTsFromCertificate(cert) + if err != nil { + return false, err + } + if len(embeddedSCTs) != 0 { + return true, nil + } + return false, nil +} + +// VerifySCT verifies SCTs against the Fulcio CT log public key. +// +// The SCT is a `Signed Certificate Timestamp`, which promises that +// the certificate issued by Fulcio was also added to the public CT log within +// some defined time period. +// +// VerifySCT can verify an SCT list embedded in the certificate, or a detached +// SCT provided by Fulcio. +// +// By default the public keys comes from TUF, but you can override this for test +// purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using +// an alternate, the file can be PEM, or DER format. +func VerifySCT(ctx context.Context, certPEM, chainPEM, rawSCT []byte) error { + // fetch SCT verification key + pubKeys := make(map[[sha256.Size]byte]logIDMetadata) + rootEnv := os.Getenv(altCTLogPublicKeyLocation) + if rootEnv == "" { + tufClient, err := tuf.NewFromEnv(ctx) + if err != nil { + return err + } + defer tufClient.Close() + + targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr}) + if err != nil { + return err + } + for _, t := range targets { + pub, err := getPublicKey(t.Target) + if err != nil { + return err + } + keyID, err := ctutil.GetCTLogID(pub) + if err != nil { + return errors.Wrap(err, "error getting CTFE public key hash") + } + pubKeys[keyID] = logIDMetadata{pub, t.Status} + } + } else { + fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv) + raw, err := os.ReadFile(rootEnv) + if err != nil { + return errors.Wrap(err, "error reading alternate public key file") + } + pubKey, err := getPublicKey(raw) + if err != nil { + return errors.Wrap(err, "error parsing alternate public key from the file") + } + keyID, err := ctutil.GetCTLogID(pubKey) + if err != nil { + return errors.Wrap(err, "error getting CTFE public key hash") + } + pubKeys[keyID] = logIDMetadata{pubKey, tuf.Active} + } + if len(pubKeys) == 0 { + return errors.New("none of the CTFE keys have been found") + } + + // parse certificate and chain + cert, err := x509util.CertificateFromPEM(certPEM) + if err != nil { + return err + } + certChain, err := x509util.CertificatesFromPEM(chainPEM) + if err != nil { + return err + } + if len(certChain) == 0 { + return errors.New("no certificate chain found") + } + + // fetch embedded SCT if present + embeddedSCTs, err := x509util.ParseSCTsFromCertificate(certPEM) + if err != nil { + return err + } + // SCT must be either embedded or in header + if len(embeddedSCTs) == 0 && len(rawSCT) == 0 { + return errors.New("no SCT found") + } + + // check SCT embedded in certificate + if len(embeddedSCTs) != 0 { + for _, sct := range embeddedSCTs { + pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID] + if !ok { + return errors.New("ctfe public key not found for embedded SCT") + } + err := ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert, certChain[0]}, sct, true) + if err != nil { + return errors.Wrap(err, "error verifying embedded SCT") + } + if pubKeyMetadata.status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified embedded SCT using an expired verification key\n") + } + } + return nil + } + + // check SCT in response header + var addChainResp ct.AddChainResponse + if err := json.Unmarshal(rawSCT, &addChainResp); err != nil { + return errors.Wrap(err, "unmarshal") + } + sct, err := addChainResp.ToSignedCertificateTimestamp() + if err != nil { + return err + } + pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID] + if !ok { + return errors.New("ctfe public key not found") + } + err = ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert}, sct, false) + if err != nil { + return errors.Wrap(err, "error verifying SCT") + } + if pubKeyMetadata.status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") + } + return nil +} + +// VerifyEmbeddedSCT verifies an embedded SCT in a certificate. +func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate) error { + if len(chain) < 2 { + return errors.New("certificate chain must contain at least a certificate and its issuer") + } + certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) + if err != nil { + return err + } + chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) + if err != nil { + return err + } + return VerifySCT(ctx, certPEM, chainPEM, []byte{}) +} + +// Given a byte array, try to construct a public key from it. +// Supports PEM encoded public keys, falling back to DER. Supports +// PKIX and PKCS1 encoded keys. +func getPublicKey(in []byte) (crypto.PublicKey, error) { + var pubKey crypto.PublicKey + var err error + var derBytes []byte + pemBlock, _ := pem.Decode(in) + if pemBlock == nil { + fmt.Fprintf(os.Stderr, "Failed to decode non-standard public key for verifying SCT using PEM decode, trying as DER") + derBytes = in + } else { + derBytes = pemBlock.Bytes + } + pubKey, err = x509.ParsePKIXPublicKey(derBytes) + if err != nil { + // Try using the PKCS1 before giving up. + pubKey, err = x509.ParsePKCS1PublicKey(derBytes) + if err != nil { + return nil, errors.Wrap(err, "failed to parse CT log public key") + } + } + return pubKey, nil +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go new file mode 100644 index 00000000000..6f6bd308784 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go @@ -0,0 +1,250 @@ +// Copyright 2022 The Sigstore 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 ctl + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "testing" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/testdata" + "github.com/google/certificate-transparency-go/tls" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +func TestGetPublicKey(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get cwd: %v", err) + } + tests := []struct { + file string + wantErrSub string + wantType string + }{ + {file: "garbage-there-are-limits", wantErrSub: "failed to parse"}, + // Testflume 2021 from here, https://letsencrypt.org/docs/ct-logs/ + {file: "letsencrypt-testflume-2021", wantType: "*ecdsa.PublicKey"}, + // This needs to be parsed with the pkcs1, pkix won't do. + {file: "rsa", wantType: "*rsa.PublicKey"}, + // This works with pkix, from: + // https://www.gstatic.com/ct/log_list/v2/log_list_pubkey.pem + {file: "google", wantType: "*rsa.PublicKey"}, + } + for _, tc := range tests { + filepath := fmt.Sprintf("%s/testdata/%s", wd, tc.file) + bytes, err := os.ReadFile(filepath) + if err != nil { + t.Fatalf("Failed to read testfile %s : %v", tc.file, err) + } + got, err := getPublicKey(bytes) + switch { + case err == nil && tc.wantErrSub != "": + t.Errorf("Wanted Error for %s but got none", tc.file) + case err != nil && tc.wantErrSub == "": + t.Errorf("Did not want error for %s but got: %v", tc.file, err) + case err != nil && tc.wantErrSub != "": + if !strings.Contains(err.Error(), tc.wantErrSub) { + t.Errorf("Unexpected error for %s: %s wanted to contain: %s", tc.file, err.Error(), tc.wantErrSub) + } + } + switch { + case got == nil && tc.wantType != "": + t.Errorf("Wanted public key for %s but got none", tc.file) + case got != nil && tc.wantType == "": + t.Errorf("Did not want error for %s but got: %v", tc.file, err) + case got != nil && tc.wantType != "": + if reflect.TypeOf(got).String() != tc.wantType { + t.Errorf("Unexpected type for %s: %+T wanted: %s", tc.file, got, tc.wantType) + } + } + } +} + +func TestContainsSCT(t *testing.T) { + // test certificate without embedded SCT + contains, err := ContainsSCT([]byte(testdata.TestCertPEM)) + if err != nil { + t.Fatalf("unexpected error in ContainsSCT: %v", err) + } + if contains { + t.Fatalf("certificate unexpectedly contained SCT") + } + + // test certificate with embedded SCT + contains, err = ContainsSCT([]byte(testdata.TestEmbeddedCertPEM)) + if err != nil { + t.Fatalf("unexpected error in ContainsSCT: %v", err) + } + if !contains { + t.Fatalf("certificate unexpectedly did not contain SCT") + } +} + +// From https://github.com/google/certificate-transparency-go/blob/e76f3f637053b90c8168d29b01ca162cd235ace5/ctutil/ctutil_test.go +func TestVerifySCT(t *testing.T) { + tests := []struct { + desc string + certPEM string + chainPEM string + sct []byte + embedded bool + wantErr bool + errMsg string + }{ + { + desc: "cert", + certPEM: testdata.TestCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "invalid SCT", + certPEM: testdata.TestPreCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + certPEM: testdata.TestEmbeddedCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + certPEM: testdata.TestInvalidEmbeddedCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + errMsg: "failed to verify ECDSA signature", + }, + } + + writePubKey(t, testdata.LogPublicKeyPEM) + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // convert SCT to response struct if detached + var sctBytes []byte + if !test.embedded { + var sct ct.SignedCertificateTimestamp + if _, err := tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + chainResp, err := toAddChainResponse(&sct) + if err != nil { + t.Fatalf("error generating chain response: %v", err) + } + sctBytes, err = json.Marshal(chainResp) + if err != nil { + t.Fatalf("error marshalling chain: %v", err) + } + } + + err := VerifySCT(context.Background(), []byte(test.certPEM), []byte(test.chainPEM), sctBytes) + if gotErr := err != nil; gotErr != test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestVerifySCTError(t *testing.T) { + // verify fails with mismatched verifcation key + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("unexpected error generating ECDSA key: %v", err) + } + pemKey, err := cryptoutils.MarshalPublicKeyToPEM(key.Public()) + if err != nil { + t.Fatalf("unexpected error marshalling ECDSA key: %v", err) + } + writePubKey(t, string(pemKey)) + err = VerifySCT(context.Background(), []byte(testdata.TestEmbeddedCertPEM), []byte(testdata.CACertPEM), []byte{}) + if err == nil || !strings.Contains(err.Error(), "ctfe public key not found") { + t.Fatalf("expected error verifying SCT with mismatched key: %v", err) + } + + // verify fails without either a detached SCT or embedded SCT + err = VerifySCT(context.Background(), []byte(testdata.TestCertPEM), []byte(testdata.CACertPEM), []byte{}) + if err == nil || !strings.Contains(err.Error(), "no SCT found") { + t.Fatalf("expected error verifying SCT without SCT: %v", err) + } +} + +func TestVerifyEmbeddedSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + // verify fails without a certificate chain + err = VerifyEmbeddedSCT(context.Background(), chain[:1]) + if err == nil || err.Error() != "certificate chain must contain at least a certificate and its issuer" { + t.Fatalf("expected error verifying SCT without chain: %v", err) + } + + writePubKey(t, testdata.LogPublicKeyPEM) + err = VerifyEmbeddedSCT(context.Background(), chain) + if err != nil { + t.Fatalf("unexpected error verifying embedded SCT: %v", err) + } +} + +// toAddChainResponse converts an SCT to a response struct, the expected structure for detached SCTs +func toAddChainResponse(sct *ct.SignedCertificateTimestamp) (*ct.AddChainResponse, error) { + sig, err := tls.Marshal(sct.Signature) + if err != nil { + return nil, fmt.Errorf("failed to marshal signature: %w", err) + } + addChainResp := &ct.AddChainResponse{ + SCTVersion: sct.SCTVersion, + Timestamp: sct.Timestamp, + Extensions: base64.StdEncoding.EncodeToString(sct.Extensions), + ID: sct.LogID.KeyID[:], + Signature: sig, + } + + return addChainResp, nil +} + +// writePubKey writes the SCT verification key to disk, since there is not a TUF +// test setup +func writePubKey(t *testing.T, keyPEM string) { + t.Helper() + + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + t.Cleanup(func() { tmpPrivFile.Close() }) + if _, err := tmpPrivFile.Write([]byte(keyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + os.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + t.Cleanup(func() { os.Unsetenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") }) +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go new file mode 100644 index 00000000000..a764b8e32f8 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go @@ -0,0 +1,224 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// 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 copied from +// https://github.com/google/certificate-transparency-go/blob/master/ctutil/ctutil.go + +// Package ctutil contains utilities for Certificate Transparency. +package ctutil + +import ( + "bytes" + "crypto" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/tls" + "github.com/google/certificate-transparency-go/x509" +) + +var emptyHash = [sha256.Size]byte{} + +// LeafHashB64 does as LeafHash does, but returns the leaf hash base64-encoded. +// The base64-encoded leaf hash returned by B64LeafHash can be used with the +// get-proof-by-hash API endpoint of Certificate Transparency Logs. +func LeafHashB64(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) (string, error) { + hash, err := LeafHash(chain, sct, embedded) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(hash[:]), nil +} + +// LeafHash calculates the leaf hash of the certificate or precertificate at +// chain[0] that sct was issued for. +// +// sct is required because the SCT timestamp is used to calculate the leaf hash. +// Leaf hashes are unique to (pre)certificate-SCT pairs. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to calculate the leaf hash for a normal X.509 +// certificate then it is enough to just provide the end entity +// certificate in chain. This case assumes that the SCT being provided is +// not embedded within the leaf certificate provided, i.e. the certificate +// is what was submitted to the Certificate Transparency Log in order to +// obtain the SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to calculate the leaf hash for a precertificate +// then the issuing certificate must also be provided in chain. The +// precertificate should be at chain[0], and its issuer at chain[1]. For +// this case, set embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If using this function to calculate the leaf hash for a certificate +// where the SCT provided is embedded within the certificate you +// are providing at chain[0], set embedded to true. LeafHash will +// calculate the leaf hash by building the corresponding precertificate. +// LeafHash will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +// +// Note: LeafHash doesn't check that the provided SCT verifies for the given +// chain. It simply calculates what the leaf hash would be for the given +// (pre)certificate-SCT pair. +func LeafHash(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) ([sha256.Size]byte, error) { + leaf, err := createLeaf(chain, sct, embedded) + if err != nil { + return emptyHash, err + } + return ct.LeafHashForLeaf(leaf) +} + +// VerifySCT takes the public key of a Certificate Transparency Log, a +// certificate chain, and an SCT and verifies whether the SCT is a valid SCT for +// the certificate at chain[0], signed by the Log that the public key belongs +// to. If the SCT does not verify, an error will be returned. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to verify an SCT for a normal X.509 certificate +// then it is enough to just provide the end entity certificate in chain. +// This case assumes that the SCT being provided is not embedded within +// the leaf certificate provided, i.e. the certificate is what was +// submitted to the Certificate Transparency Log in order to obtain the +// SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to verify an SCT for a precertificate then the +// issuing certificate must also be provided in chain. The precertificate +// should be at chain[0], and its issuer at chain[1]. For this case, set +// embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If the SCT you wish to verify is embedded within the certificate you +// are providing at chain[0], set embedded to true. VerifySCT will +// verify the provided SCT by building the corresponding precertificate. +// VerifySCT will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +func VerifySCT(pubKey crypto.PublicKey, chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) error { + s, err := ct.NewSignatureVerifier(pubKey) + if err != nil { + return fmt.Errorf("error creating signature verifier: %w", err) + } + + return VerifySCTWithVerifier(s, chain, sct, embedded) +} + +// VerifySCTWithVerifier takes a ct.SignatureVerifier, a certificate chain, and +// an SCT and verifies whether the SCT is a valid SCT for the certificate at +// chain[0], signed by the Log whose public key was used to set up the +// ct.SignatureVerifier. If the SCT does not verify, an error will be returned. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to verify an SCT for a normal X.509 certificate +// then it is enough to just provide the end entity certificate in chain. +// This case assumes that the SCT being provided is not embedded within +// the leaf certificate provided, i.e. the certificate is what was +// submitted to the Certificate Transparency Log in order to obtain the +// SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to verify an SCT for a precertificate then the +// issuing certificate must also be provided in chain. The precertificate +// should be at chain[0], and its issuer at chain[1]. For this case, set +// embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If the SCT you wish to verify is embedded within the certificate you +// are providing at chain[0], set embedded to true. VerifySCT will +// verify the provided SCT by building the corresponding precertificate. +// VerifySCT will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +func VerifySCTWithVerifier(sv *ct.SignatureVerifier, chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) error { + if sv == nil { + return errors.New("ct.SignatureVerifier is nil") + } + + leaf, err := createLeaf(chain, sct, embedded) + if err != nil { + return err + } + + return sv.VerifySCTSignature(*sct, ct.LogEntry{Leaf: *leaf}) +} + +func createLeaf(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) (*ct.MerkleTreeLeaf, error) { + if len(chain) == 0 { + return nil, errors.New("chain is empty") + } + if sct == nil { + return nil, errors.New("sct is nil") + } + + if embedded { + sctPresent, err := ContainsSCT(chain[0], sct) + if err != nil { + return nil, fmt.Errorf("error checking for SCT in leaf certificate: %w", err) + } + if !sctPresent { + return nil, errors.New("SCT provided is not embedded within leaf certificate") + } + } + + certType := ct.X509LogEntryType + if chain[0].IsPrecertificate() || embedded { + certType = ct.PrecertLogEntryType + } + + var leaf *ct.MerkleTreeLeaf + var err error + if embedded { + leaf, err = ct.MerkleTreeLeafForEmbeddedSCT(chain, sct.Timestamp) + } else { + leaf, err = ct.MerkleTreeLeafFromChain(chain, certType, sct.Timestamp) + } + if err != nil { + return nil, fmt.Errorf("error creating MerkleTreeLeaf: %w", err) + } + return leaf, nil +} + +// ContainsSCT checks to see whether the given SCT is embedded within the given +// certificate. +func ContainsSCT(cert *x509.Certificate, sct *ct.SignedCertificateTimestamp) (bool, error) { + if cert == nil || sct == nil { + return false, nil + } + + sctBytes, err := tls.Marshal(*sct) + if err != nil { + return false, fmt.Errorf("error tls.Marshalling SCT: %w", err) + } + for _, s := range cert.SCTList.SCTList { + if bytes.Equal(sctBytes, s.Val) { + return true, nil + } + } + return false, nil +} + +// GetCTLogID takes the key manager for a log and returns the LogID. (see RFC 6962 S3.2) +// In CT V1 the log id is a hash of the public key. +func GetCTLogID(pk crypto.PublicKey) ([sha256.Size]byte, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + return [sha256.Size]byte{}, err + } + return sha256.Sum256(pubBytes), nil +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go new file mode 100644 index 00000000000..8476d866d0c --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go @@ -0,0 +1,385 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// 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 ctutil + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "testing" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/testdata" + "github.com/google/certificate-transparency-go/tls" + ttestdata "github.com/google/certificate-transparency-go/trillian/testdata" + "github.com/google/certificate-transparency-go/x509util" +) + +var ( + demoLogID = [32]byte{19, 56, 222, 93, 229, 36, 102, 128, 227, 214, 3, 121, 93, 175, 126, 236, 97, 217, 34, 32, 40, 233, 98, 27, 46, 179, 164, 251, 84, 10, 60, 57} +) + +func TestLeafHash(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + want string + }{ + { + desc: "cert", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + want: testdata.TestCertB64LeafHash, + }, + { + desc: "precert", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + want: testdata.TestPreCertB64LeafHash, + }, + { + desc: "cert with embedded SCT", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + want: testdata.TestPreCertB64LeafHash, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test LeafHash() + wantSl, err := base64.StdEncoding.DecodeString(test.want) + if err != nil { + t.Fatalf("error base64-decoding leaf hash %q: %s", test.want, err) + } + var want [32]byte + copy(want[:], wantSl) + + got, err := LeafHash(chain, &sct, test.embedded) + if got != want || err != nil { + t.Errorf("LeafHash(_,_) = %v, %v, want %v, nil", got, err, want) + } + + // Test LeafHashB64() + gotB64, err := LeafHashB64(chain, &sct, test.embedded) + if gotB64 != test.want || err != nil { + t.Errorf("LeafHashB64(_,_) = %v, %v, want %v, nil", gotB64, err, test.want) + } + }) + } +} + +func TestLeafHashErrors(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + }{ + { + desc: "empty chain", + chainPEM: "", + sct: testdata.TestCertProof, + }, + { + desc: "nil SCT", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: nil, + }, + { + desc: "no SCTs embedded in cert, embedded true", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + }, + { + desc: "cert contains embedded SCTs, but not the SCT provided", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct *ct.SignedCertificateTimestamp + if test.sct != nil { + sct = &ct.SignedCertificateTimestamp{} + if _, err = tls.Unmarshal(test.sct, sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + } + + // Test LeafHash() + got, err := LeafHash(chain, sct, test.embedded) + if got != emptyHash || err == nil { + t.Errorf("LeafHash(_,_) = %s, %v, want %v, error", got, err, emptyHash) + } + + // Test LeafHashB64() + gotB64, err := LeafHashB64(chain, sct, test.embedded) + if gotB64 != "" || err == nil { + t.Errorf("LeafHashB64(_,_) = %s, %v, want \"\", error", gotB64, err) + } + }) + } +} + +func TestVerifySCT(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + wantErr bool + }{ + { + desc: "cert", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "precert", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + }, + { + desc: "invalid SCT", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + chainPEM: testdata.TestInvalidEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test VerifySCT() + pk, err := ct.PublicKeyFromB64(testdata.LogPublicKeyB64) + if err != nil { + t.Errorf("error parsing public key: %s", err) + } + + err = VerifySCT(pk, chain, &sct, test.embedded) + if gotErr := err != nil; gotErr != test.wantErr { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestVerifySCTWithVerifier(t *testing.T) { + // Parse public key + pk, err := ct.PublicKeyFromB64(testdata.LogPublicKeyB64) + if err != nil { + t.Errorf("error parsing public key: %s", err) + } + + // Create signature verifier + sv, err := ct.NewSignatureVerifier(pk) + if err != nil { + t.Errorf("couldn't create signature verifier: %s", err) + } + + tests := []struct { + desc string + sv *ct.SignatureVerifier + chainPEM string + sct []byte + embedded bool + wantErr bool + }{ + { + desc: "nil signature verifier", + sv: nil, + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert", + sv: sv, + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "precert", + sv: sv, + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + }, + { + desc: "invalid SCT", + sv: sv, + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + sv: sv, + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + sv: sv, + chainPEM: testdata.TestInvalidEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test VerifySCTWithVerifier() + err = VerifySCTWithVerifier(test.sv, chain, &sct, test.embedded) + if gotErr := err != nil; gotErr != test.wantErr { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestContainsSCT(t *testing.T) { + tests := []struct { + desc string + certPEM string + sct []byte + want bool + }{ + { + desc: "cert doesn't contain any SCTs", + certPEM: testdata.TestCertPEM, + sct: testdata.TestPreCertProof, + want: false, + }, + { + desc: "cert contains SCT but not specified SCT", + certPEM: testdata.TestEmbeddedCertPEM, + sct: testdata.TestInvalidProof, + want: false, + }, + { + desc: "cert contains SCT", + certPEM: testdata.TestEmbeddedCertPEM, + sct: testdata.TestPreCertProof, + want: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse cert + cert, err := x509util.CertificateFromPEM([]byte(test.certPEM)) + if err != nil { + t.Fatalf("error parsing certificate: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test ContainsSCT() + got, err := ContainsSCT(cert, &sct) + if err != nil { + t.Fatalf("ContainsSCT(_,_) = false, %s, want no error", err) + } + + if got != test.want { + t.Errorf("ContainsSCT(_,_) = %t, nil, want %t, nil", got, test.want) + } + }) + } +} + +func TestGetCTLogID(t *testing.T) { + block, _ := pem.Decode([]byte(ttestdata.DemoPublicKey)) + pk, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + t.Fatalf("unexpected error loading public key: %v", err) + } + + got, err := GetCTLogID(pk) + if err != nil { + t.Fatalf("error getting logid: %v", err) + } + + if want := demoLogID; got != want { + t.Errorf("logID: \n%v want \n%v", got, want) + } +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 819585a9985..3687f5db01f 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -17,103 +17,16 @@ package fulcioverifier import ( "context" - "crypto" - "crypto/x509" - "encoding/json" - "encoding/pem" "fmt" "os" - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/ctutil" - ctx509 "github.com/google/certificate-transparency-go/x509" - "github.com/google/certificate-transparency-go/x509util" "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" - "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/fulcio/pkg/api" ) -// This is the CT log public key target name -var ctPublicKeyStr = `ctfe.pub` - -// Setting this env variable will over ride what is used to validate -// the SCT coming back from Fulcio. -const altCTLogPublicKeyLocation = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" - -// verifySCT verifies the SCT against the Fulcio CT log public key. -// By default this comes from TUF, but you can override this (for test) -// purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using -// an alternate, the file can be PEM, or DER format. -// -// The SCT is a `Signed Certificate Timestamp`, which promises that -// the certificate issued by Fulcio was also added to the public CT log within -// some defined time period -func verifySCT(ctx context.Context, certPEM, rawSCT []byte) error { - pubKeys := make(map[crypto.PublicKey]tuf.StatusKind) - rootEnv := os.Getenv(altCTLogPublicKeyLocation) - if rootEnv == "" { - tufClient, err := tuf.NewFromEnv(ctx) - if err != nil { - return err - } - defer tufClient.Close() - - targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr}) - if err != nil { - return err - } - for _, t := range targets { - ctPub, err := cosign.PemToECDSAKey(t.Target) - if err != nil { - return errors.Wrap(err, "converting Public CT to ECDSAKey") - } - pubKeys[ctPub] = t.Status - } - } else { - fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv) - raw, err := os.ReadFile(rootEnv) - if err != nil { - return errors.Wrap(err, "error reading alternate public key file") - } - pubKey, err := getAlternatePublicKey(raw) - if err != nil { - return errors.Wrap(err, "error parsing alternate public key from the file") - } - pubKeys[pubKey] = tuf.Active - } - if len(pubKeys) == 0 { - return errors.New("none of the CTFE keys have been found") - } - cert, err := x509util.CertificateFromPEM(certPEM) - if err != nil { - return err - } - var addChainResp ct.AddChainResponse - if err := json.Unmarshal(rawSCT, &addChainResp); err != nil { - return errors.Wrap(err, "unmarshal") - } - sct, err := addChainResp.ToSignedCertificateTimestamp() - if err != nil { - return err - } - var verifySctErr error - for pubKey, status := range pubKeys { - verifySctErr = ctutil.VerifySCT(pubKey, []*ctx509.Certificate{cert}, sct, false) - // Exit after successful verification of the SCT - if verifySctErr == nil { - if status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") - } - return nil - } - } - // Return the last error that occurred during verification. - return verifySctErr -} - func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.Client) (*fulcio.Signer, error) { fs, err := fulcio.NewSigner(ctx, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL, fClient) if err != nil { @@ -121,35 +34,10 @@ func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClien } // verify the sct - if err := verifySCT(ctx, fs.Cert, fs.SCT); err != nil { + if err := ctl.VerifySCT(ctx, fs.Cert, fs.Chain, fs.SCT); err != nil { return nil, errors.Wrap(err, "verifying SCT") } fmt.Fprintln(os.Stderr, "Successfully verified SCT...") return fs, nil } - -// Given a byte array, try to construct a public key from it. -// Will try first to see if it's PEM formatted, if not, then it will -// try to parse it as der publics, and failing that -func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) { - var pubKey crypto.PublicKey - var err error - var derBytes []byte - pemBlock, _ := pem.Decode(in) - if pemBlock == nil { - fmt.Fprintf(os.Stderr, "Failed to decode non-standard public key for verifying SCT using PEM decode, trying as DER") - derBytes = in - } else { - derBytes = pemBlock.Bytes - } - pubKey, err = x509.ParsePKIXPublicKey(derBytes) - if err != nil { - // Try using the PKCS1 before giving up. - pubKey, err = x509.ParsePKCS1PublicKey(derBytes) - if err != nil { - return nil, errors.Wrap(err, "failed to parse alternate public key") - } - } - return pubKey, nil -} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go deleted file mode 100644 index a4588e7ace5..00000000000 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// -// Copyright 2021 The Sigstore 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 fulcioverifier - -import ( - "fmt" - "os" - "reflect" - "strings" - "testing" -) - -func TestGetAlternatePublicKey(t *testing.T) { - wd, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get cwd: %v", err) - } - tests := []struct { - file string - wantErrSub string - wantType string - }{ - {file: "garbage-there-are-limits", wantErrSub: "failed to parse"}, - // Testflume 2021 from here, https://letsencrypt.org/docs/ct-logs/ - {file: "letsencrypt-testflume-2021", wantType: "*ecdsa.PublicKey"}, - // This needs to be parsed with the pkcs1, pkix won't do. - {file: "rsa", wantType: "*rsa.PublicKey"}, - // This works with pkix, from: - // https://www.gstatic.com/ct/log_list/v2/log_list_pubkey.pem - {file: "google", wantType: "*rsa.PublicKey"}, - } - for _, tc := range tests { - filepath := fmt.Sprintf("%s/testdata/%s", wd, tc.file) - bytes, err := os.ReadFile(filepath) - if err != nil { - t.Fatalf("Failed to read testfile %s : %v", tc.file, err) - } - got, err := getAlternatePublicKey(bytes) - switch { - case err == nil && tc.wantErrSub != "": - t.Errorf("Wanted Error for %s but got none", tc.file) - case err != nil && tc.wantErrSub == "": - t.Errorf("Did not want error for %s but got: %v", tc.file, err) - case err != nil && tc.wantErrSub != "": - if !strings.Contains(err.Error(), tc.wantErrSub) { - t.Errorf("Unexpected error for %s: %s wanted to contain: %s", tc.file, err.Error(), tc.wantErrSub) - } - } - switch { - case got == nil && tc.wantType != "": - t.Errorf("Wanted public key for %s but got none", tc.file) - case got != nil && tc.wantType == "": - t.Errorf("Did not want error for %s but got: %v", tc.file, err) - case got != nil && tc.wantType != "": - if reflect.TypeOf(got).String() != tc.wantType { - t.Errorf("Unexpected type for %s: %+T wanted: %s", tc.file, got, tc.wantType) - } - } - } -} diff --git a/cmd/cosign/cli/manifest.go b/cmd/cosign/cli/manifest.go index 07c213cbe58..5854065f43e 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -87,6 +87,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index f801f4f801a..615842c810e 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -24,6 +24,7 @@ type CertVerifyOptions struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool } var _ Interface = (*RekorOptions)(nil) @@ -44,4 +45,8 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { "when building the certificate chain for the signing certificate. "+ "Must start with the parent intermediate CA certificate of the "+ "signing certificate and end with the root certificate") + + cmd.Flags().BoolVar(&o.EnforceSCT, "enforce-sct", false, + "whether to enforce that a certificate contain an embedded SCT, a proof of "+ + "inclusion in a certificate transparency log") } diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 6e3076f566d..a039fe3402a 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -47,6 +47,9 @@ func Sign() *cobra.Command { # sign a container image and add annotations cosign sign --key cosign.key -a key1=value1 -a key2=value2 + # sign a container image with a key stored in an environment variable + cosign sign --key env://[ENV_VAR] + # sign a container image with a key pair stored in Azure Key Vault cosign sign --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 4f3a12843ed..44da104baf8 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -32,6 +32,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" icos "github.com/sigstore/cosign/internal/pkg/cosign" @@ -414,9 +415,22 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin for _, c := range certChain[:len(certChain)-1] { subPool.AddCert(c) } - if err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { + if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { return nil, errors.Wrap(err, "unable to validate certificate chain") } + // Verify SCT if present in the leaf certificate. + contains, err := ctl.ContainsSCT(leafCert.Raw) + if err != nil { + return nil, err + } + if contains { + var chain []*x509.Certificate + chain = append(chain, leafCert) + chain = append(chain, certChain...) + if err := ctl.VerifyEmbeddedSCT(context.Background(), chain); err != nil { + return nil, err + } + } certSigner.Chain = certChainBytes return certSigner, nil diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 680e30239a1..dc75b8e3f45 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -62,6 +62,9 @@ against the transparency log.`, # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] + # verify image with a key stored in an environment variable + cosign verify --key env://[ENV_VAR] + # verify image with public key stored in Google Cloud KMS cosign verify --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] @@ -97,6 +100,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, @@ -174,6 +178,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, @@ -252,7 +257,8 @@ The blob may be specified as a path to a file or - for stdin.`, BundlePath: o.BundlePath, } if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert, - o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, o.Signature, args[0]); err != nil { + o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, + o.Signature, args[0], o.CertVerify.EnforceSCT); err != nil { return errors.Wrapf(err, "verifying blob %s", args) } return nil diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 79cc1325c6d..fb9595fd54a 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -54,6 +54,7 @@ type VerifyCommand struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool Sk bool Slot string Output string @@ -95,6 +96,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, SignatureRef: c.SignatureRef, } if c.CheckClaims { @@ -109,6 +111,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { co.RekorClient = rekorClient } co.RootCerts = fulcio.GetRoots() + co.IntermediateCerts = fulcio.GetIntermediates() } keyRef := c.KeyRef certRef := c.CertRef diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index e868eb8fb11..93867633976 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -18,15 +18,12 @@ package verify import ( "context" "crypto" - "encoding/base64" - "encoding/json" "flag" "fmt" "os" "path/filepath" "github.com/google/go-containerregistry/pkg/name" - "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/pkg/cosign/rego" @@ -39,6 +36,7 @@ import ( "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/cue" "github.com/sigstore/cosign/pkg/cosign/pivkey" + "github.com/sigstore/cosign/pkg/policy" sigs "github.com/sigstore/cosign/pkg/signature" ) @@ -47,11 +45,12 @@ import ( type VerifyAttestationCommand struct { options.RegistryOptions CheckClaims bool + KeyRef string CertRef string CertEmail string CertOidcIssuer string CertChain string - KeyRef string + EnforceSCT bool Sk bool Slot string Output string @@ -79,6 +78,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier @@ -92,6 +92,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e co.RekorClient = rekorClient } co.RootCerts = fulcio.GetRoots() + co.IntermediateCerts = fulcio.GetIntermediates() } keyRef := c.KeyRef @@ -182,80 +183,15 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e var validationErrors []error for _, vp := range verified { - var payloadData map[string]interface{} - - p, err := vp.Payload() - if err != nil { - return errors.Wrap(err, "could not get payload") - } - - err = json.Unmarshal(p, &payloadData) + payload, err := policy.AttestationToPayloadJSON(ctx, c.PredicateType, vp) if err != nil { - return errors.Wrap(err, "unmarshal payload data") + return errors.Wrap(err, "converting to consumable policy validation") } - - var decodedPayload []byte - if val, ok := payloadData["payload"]; ok { - decodedPayload, err = base64.StdEncoding.DecodeString(val.(string)) - if err != nil { - return fmt.Errorf("could not decode 'payload': %w", err) - } - } else { - return fmt.Errorf("could not find 'payload' in payload data") - } - - predicateURI, ok := options.PredicateTypeMap[c.PredicateType] - if !ok { - return fmt.Errorf("invalid predicate type: %s", c.PredicateType) - } - - // Only apply the policy against the requested predicate type - var statement in_toto.Statement - if err := json.Unmarshal(decodedPayload, &statement); err != nil { - return fmt.Errorf("unmarshal in-toto statement: %w", err) - } - if statement.PredicateType != predicateURI { + if len(payload) == 0 { + // This is not the predicate type we're looking for. continue } - var payload []byte - switch c.PredicateType { - case options.PredicateCustom: - payload, err = json.Marshal(statement) - if err != nil { - return fmt.Errorf("error when generating CosignStatement: %w", err) - } - case options.PredicateLink: - var linkStatement in_toto.LinkStatement - if err := json.Unmarshal(decodedPayload, &linkStatement); err != nil { - return fmt.Errorf("unmarshal LinkStatement: %w", err) - } - payload, err = json.Marshal(linkStatement) - if err != nil { - return fmt.Errorf("error when generating LinkStatement: %w", err) - } - case options.PredicateSLSA: - var slsaProvenanceStatement in_toto.ProvenanceStatement - if err := json.Unmarshal(decodedPayload, &slsaProvenanceStatement); err != nil { - return fmt.Errorf("unmarshal ProvenanceStatement: %w", err) - } - payload, err = json.Marshal(slsaProvenanceStatement) - if err != nil { - return fmt.Errorf("error when generating ProvenanceStatement: %w", err) - } - case options.PredicateSPDX: - var spdxStatement in_toto.SPDXStatement - if err := json.Unmarshal(decodedPayload, &spdxStatement); err != nil { - return fmt.Errorf("unmarshal SPDXStatement: %w", err) - } - payload, err = json.Marshal(spdxStatement) - if err != nil { - return fmt.Errorf("error when generating SPDXStatement: %w", err) - } - default: - return fmt.Errorf("unsupported predicate type: %s", c.PredicateType) - } - if len(cuePolicies) > 0 { fmt.Fprintf(os.Stderr, "will be validating against CUE policies: %v\n", cuePolicies) cueValidationErr := cue.ValidateJSON(payload, cuePolicies) diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 8cc570be31f..0341ed0a025 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -61,7 +61,8 @@ func isb64(data []byte) bool { } // nolint -func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, certChain, sigRef, blobRef string) error { +func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, + certOidcIssuer, certChain, sigRef, blobRef string, enforceSCT bool) error { var verifier signature.Verifier var cert *x509.Certificate @@ -119,6 +120,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer co := &cosign.CheckOpts{ CertEmail: certEmail, CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) if err != nil { @@ -162,7 +164,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer if len(uuids) == 0 { return errors.New("could not find a tlog entry for provided blob") } - return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes) + return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes, enforceSCT) } // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. @@ -184,7 +186,8 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer return nil } -func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, uuids []string, blobBytes []byte) error { +func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, + uuids []string, blobBytes []byte, enforceSCT bool) error { var validSigExists bool for _, u := range uuids { tlogEntry, err := cosign.GetTlogEntry(ctx, rClient, u) @@ -198,9 +201,11 @@ func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor } co := &cosign.CheckOpts{ - RootCerts: fulcio.GetRoots(), - CertEmail: certEmail, - CertOidcIssuer: certOidcIssuer, + RootCerts: fulcio.GetRoots(), + IntermediateCerts: fulcio.GetIntermediates(), + CertEmail: certEmail, + CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } cert := certs[0] verifier, err := cosign.ValidateAndUnpackCert(cert, co) @@ -351,19 +356,16 @@ func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certif return errors.Wrap(err, "retrieving rekor public key") } - var entryVerError error - for _, pubKey := range publicKeys { - entryVerError = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey) - // Exit early with successful verification - if entryVerError == nil { - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - break - } + pubKey, ok := publicKeys[b.Bundle.Payload.LogID] + if !ok { + return errors.New("rekor log public key not found for payload") + } + err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey) + if err != nil { + return err } - if entryVerError != nil { - return entryVerError + if pubKey.Status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") } if cert == nil { diff --git a/cmd/cosign/main.go b/cmd/cosign/main.go index d6d1b89c230..00bb0b81ede 100644 --- a/cmd/cosign/main.go +++ b/cmd/cosign/main.go @@ -21,6 +21,12 @@ import ( "strings" "github.com/sigstore/cosign/cmd/cosign/cli" + + // Register the provider-specific plugins + _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" + _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" + _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" + _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) func main() { diff --git a/cmd/cosign/policy_webhook/main.go b/cmd/cosign/policy_webhook/main.go index da136e19303..54a90fa6dcd 100644 --- a/cmd/cosign/policy_webhook/main.go +++ b/cmd/cosign/policy_webhook/main.go @@ -34,6 +34,31 @@ import ( "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" "github.com/sigstore/cosign/pkg/reconciler/clusterimagepolicy" + + // Register the provider-specific plugins + _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" + _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" + _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" + _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" +) + +var ( + // mutatingWebhookName holds the name of the mutating webhook configuration + // resource dispatching admission requests to policy-webhook. + // It is also the name of the webhook which is injected by the controller + // with the resource types, namespace selectors, CABindle and service path. + // If this changes, you must also change: + // ./config/501-policy-webhook-configurations.yaml + // https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/policy-webhook/policy_webhook_configurations.yaml + mutatingWebhookName = flag.String("mutating-webhook-name", "defaulting.clusterimagepolicy.sigstore.dev", "The name of the mutating webhook configuration as well as the webhook name that is automatically configured, if exists, with different rules and client settings setting how the admission requests to be dispatched to policy-webhook.") + // validatingWebhookName holds the name of the validating webhook configuration + // resource dispatching admission requests to policy-webhook. + // It is also the name of the webhook which is injected by the controller + // with the resource types, namespace selectors, CABindle and service path. + // If this changes, you must also change: + // ./config/501-policy-webhook-configurations.yaml + // https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/policy-webhook/policy_webhook_configurations.yaml + validatingWebhookName = flag.String("validating-webhook-name", "validating.clusterimagepolicy.sigstore.dev", "The name of the validating webhook configuration as well as the webhook name that is automatically configured, if exists, with different rules and client settings setting how the admission requests to be dispatched to policy-webhook.") ) func main() { @@ -62,7 +87,7 @@ func main() { func NewPolicyValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { return validation.NewAdmissionController( ctx, - "validating.clusterimagepolicy.sigstore.dev", + *validatingWebhookName, "/validating", map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ v1alpha1.SchemeGroupVersion.WithKind("ClusterImagePolicy"): &v1alpha1.ClusterImagePolicy{}, @@ -77,7 +102,7 @@ func NewPolicyValidatingAdmissionController(ctx context.Context, cmw configmap.W func NewPolicyMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { return defaulting.NewAdmissionController( ctx, - "defaulting.clusterimagepolicy.sigstore.dev", + *mutatingWebhookName, "/defaulting", map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ v1alpha1.SchemeGroupVersion.WithKind("ClusterImagePolicy"): &v1alpha1.ClusterImagePolicy{}, diff --git a/cmd/cosign/webhook/main.go b/cmd/cosign/webhook/main.go index 5c99513f5ee..e75f8d75de0 100644 --- a/cmd/cosign/webhook/main.go +++ b/cmd/cosign/webhook/main.go @@ -44,10 +44,15 @@ import ( var secretName = flag.String("secret-name", "", "The name of the secret in the webhook's namespace that holds the public key for verification.") -// webhookName holds the name of the validating webhook to set up with the -// types we are watching. If this changes, you must also change: +// webhookName holds the name of the validating and mutating webhook +// configuration resources dispatching admission requests to cosigned. +// It is also the name of the webhook which is injected by the controller +// with the resource types, namespace selectors, CABindle and service path. +// If this changes, you must also change: // ./config/500-webhook-configuration.yaml -const webhookName = "cosigned.sigstore.dev" +// https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/webhook/webhook_mutating.yaml +// https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/webhook/webhook_validating.yaml +var webhookName = flag.String("webhook-name", "cosigned.sigstore.dev", "The name of the validating and mutating webhook configurations as well as the webhook name that is automatically configured, if exists, with different rules and client settings setting how the admission requests to be dispatched to cosigned.") func main() { opts := webhook.Options{ @@ -92,7 +97,7 @@ func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher return validation.NewAdmissionController(ctx, // Name of the resource webhook. - webhookName, + *webhookName, // The path on which to serve the webhook. "/validations", @@ -123,7 +128,7 @@ func NewMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) return defaulting.NewAdmissionController(ctx, // Name of the resource webhook. - webhookName, + *webhookName, // The path on which to serve the webhook. "/mutations", diff --git a/config/300-clusterimagepolicy.yaml b/config/300-clusterimagepolicy.yaml index 5301b2cb5fa..cd5545806f1 100644 --- a/config/300-clusterimagepolicy.yaml +++ b/config/300-clusterimagepolicy.yaml @@ -44,6 +44,36 @@ spec: items: type: object properties: + attestations: + type: array + items: + type: object + properties: + name: + description: Name of the attestation. These can then be referenced at the CIP level policy. + type: string + policy: + type: object + properties: + configMapRef: + type: object + properties: + name: + description: Name is unique within a namespace to reference a configmap resource. + type: string + namespace: + description: Namespace defines the space within which the configmap name must be unique. + type: string + data: + type: string + type: + description: Which kind of policy this is, currently only rego or cue are supported. Furthermore, only cue is tested :) + type: string + url: + type: string + predicateType: + description: Which predicate type to verify. Matches cosign verify-attestation options. + type: string ctlog: type: object properties: @@ -99,6 +129,9 @@ spec: type: string url: type: string + name: + description: Name is the name for this authority. Used by the CIP Policy validator to be able to reference matching signature or attestation verifications. If not specified, the name will be authority- + type: string source: type: array items: @@ -115,3 +148,23 @@ spec: type: string regex: type: string + policy: + description: Policy is an optional policy that can be applied against all the successfully validated Authorities. If no authorities pass, this does not even get evaluated, as the Policy is considered failed. + type: object + properties: + configMapRef: + type: object + properties: + name: + description: Name is unique within a namespace to reference a configmap resource. + type: string + namespace: + description: Namespace defines the space within which the configmap name must be unique. + type: string + data: + type: string + type: + description: Which kind of policy this is, currently only rego or cue are supported. Furthermore, only cue is tested :) + type: string + url: + type: string diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index bf0ae43bf8d..fa6ec41bfe3 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -62,6 +62,7 @@ cosign dockerfile verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index a78e08bfb59..81f314d83f8 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -56,6 +56,7 @@ cosign manifest verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_sign.md b/doc/cosign_sign.md index c4d3f26e0ff..88230c10524 100644 --- a/doc/cosign_sign.md +++ b/doc/cosign_sign.md @@ -27,6 +27,9 @@ cosign sign [flags] # sign a container image and add annotations cosign sign --key cosign.key -a key1=value1 -a key2=value2 + # sign a container image with a key stored in an environment variable + cosign sign --key env://[ENV_VAR] + # sign a container image with a key pair stored in Azure Key Vault cosign sign --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 90c7e7a4f09..65ca6994699 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -66,6 +66,7 @@ cosign verify-attestation [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-attestation --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index cb02172985a..894e46afde6 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -68,6 +68,7 @@ cosign verify-blob [flags] --cert-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-blob --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index c27f4d50808..9de4dbad45f 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -44,6 +44,9 @@ cosign verify [flags] # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] + # verify image with a key stored in an environment variable + cosign verify --key env://[ENV_VAR] + # verify image with public key stored in Google Cloud KMS cosign verify --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] @@ -72,6 +75,7 @@ cosign verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/go.mod b/go.mod index 57826b43872..badbca06cd7 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( cloud.google.com/go/storage v1.22.0 - cuelang.org/go v0.4.2 + cuelang.org/go v0.4.3 github.com/ThalesIgnite/crypto11 v1.2.5 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795 github.com/cenkalti/backoff/v3 v3.2.2 @@ -21,9 +21,9 @@ require ( github.com/google/go-github/v42 v42.0.0 github.com/google/trillian v1.4.0 github.com/hashicorp/go-cleanhttp v0.5.2 - github.com/hashicorp/go-retryablehttp v0.7.0 + github.com/hashicorp/go-retryablehttp v0.7.1 github.com/hashicorp/go-rootcerts v1.0.2 - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf github.com/kelseyhightower/envconfig v1.4.0 @@ -38,20 +38,20 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.3.1 github.com/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 - github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f + github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5 github.com/spf13/cobra v1.4.0 - github.com/spf13/viper v1.10.1 + github.com/spf13/viper v1.11.0 github.com/spiffe/go-spiffe/v2 v2.0.0 github.com/stretchr/testify v1.7.1 github.com/theupdateframework/go-tuf v0.0.0-20220211205608-f0c3294f63b9 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 - github.com/xanzy/go-gitlab v0.62.0 - golang.org/x/net v0.0.0-20220325170049-de3da57026de - golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a - golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 + github.com/xanzy/go-gitlab v0.64.0 + golang.org/x/net v0.0.0-20220412020605-290c469a71a5 + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 - google.golang.org/api v0.74.0 + google.golang.org/api v0.75.0 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.23.5 @@ -86,20 +86,20 @@ require ( github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 - github.com/mitchellh/mapstructure v1.4.3 + github.com/mitchellh/mapstructure v1.5.0 github.com/pierrec/lz4 v2.6.1+incompatible github.com/prometheus/procfs v0.7.3 // indirect - github.com/spf13/afero v1.8.0 // indirect + github.com/spf13/afero v1.8.2 // indirect github.com/urfave/cli v1.22.5 // indirect github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898 go.opentelemetry.io/contrib v1.3.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect go.uber.org/atomic v1.9.0 go.uber.org/zap v1.21.0 - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 - google.golang.org/grpc v1.45.0 + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 + google.golang.org/grpc v1.46.0 google.golang.org/protobuf v1.28.0 - k8s.io/code-generator v0.23.5 + k8s.io/code-generator v0.23.6 k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf knative.dev/hack v0.0.0-20220224013837-e1785985d364 knative.dev/hack/schema v0.0.0-20220224013837-e1785985d364 @@ -108,14 +108,14 @@ require ( require ( bitbucket.org/creachadair/shell v0.0.6 // indirect cloud.google.com/go v0.100.2 // indirect - cloud.google.com/go/compute v1.5.0 // indirect + cloud.google.com/go/compute v1.6.0 // indirect cloud.google.com/go/iam v0.3.0 // indirect cloud.google.com/go/kms v1.4.0 // indirect contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect - github.com/Azure/azure-sdk-for-go v63.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.25 // indirect + github.com/Azure/go-autorest/autorest v0.11.27 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect @@ -131,7 +131,7 @@ require ( github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect - github.com/aws/aws-sdk-go v1.43.30 // indirect + github.com/aws/aws-sdk-go v1.43.45 // indirect github.com/aws/aws-sdk-go-v2 v1.14.0 // indirect github.com/aws/aws-sdk-go-v2/config v1.14.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.9.0 // indirect @@ -172,7 +172,7 @@ require ( github.com/dustin/go-humanize v1.0.0 // indirect github.com/emicklei/go-restful v2.9.5+incompatible // indirect github.com/emicklei/proto v1.6.15 // indirect - github.com/envoyproxy/go-control-plane v0.10.1 // indirect + github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect @@ -207,7 +207,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gax-go/v2 v2.2.0 // indirect + github.com/googleapis/gax-go/v2 v2.3.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/googleapis/go-type-adapters v1.0.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect @@ -225,7 +225,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.14.2 // indirect github.com/leodido/go-urn v1.2.1 // indirect - github.com/magiconair/properties v1.8.5 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect @@ -241,6 +241,7 @@ require ( github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -271,9 +272,9 @@ require ( github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect github.com/zeebo/errs v1.2.2 // indirect go.etcd.io/bbolt v1.3.6 // indirect - go.etcd.io/etcd/api/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/v2 v2.305.1 // indirect + go.etcd.io/etcd/api/v3 v3.5.2 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect + go.etcd.io/etcd/client/v2 v2.305.2 // indirect go.etcd.io/etcd/client/v3 v3.5.0 // indirect go.etcd.io/etcd/etcdctl/v3 v3.5.0 // indirect go.etcd.io/etcd/etcdutl/v3 v3.5.0 // indirect @@ -298,13 +299,13 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.10 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect + google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect k8s.io/apiextensions-apiserver v0.23.4 // indirect @@ -313,3 +314,5 @@ require ( sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) + +exclude github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b diff --git a/go.sum b/go.sum index ec81bc735db..ba585bdd774 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,9 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0 h1:XdQIN5mdPTSBVwSIVDuY5e8ZzVAccsHvD3qTEz4zIps= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -108,8 +109,8 @@ contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= -cuelang.org/go v0.4.2 h1:l+ptgjryFJ/aikhEMSem36LoWkNi6YNFmsERW2hgww4= -cuelang.org/go v0.4.2/go.mod h1:P09/R4UfAEzLkV9DXxwlxQnIZbkaT4uIhiEgs6Vsz2Q= +cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= +cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -130,8 +131,8 @@ github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v60.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v60.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v62.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v63.0.0+incompatible h1:whPsa+jCHQSo5wGMPNLw4bz8q9Co2+vnXHzXGctoTaQ= -github.com/Azure/azure-sdk-for-go v63.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v63.3.0+incompatible h1:INepVujzUrmArRZjDLHbtER+FkvCoEwyRCXGqOlmDII= +github.com/Azure/azure-sdk-for-go v63.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= @@ -152,8 +153,8 @@ github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgq github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest v0.11.25 h1:yp+V8DGur2aIUE87ebP8twPLz6k68jtJTlg61mEoByA= -github.com/Azure/go-autorest/autorest v0.11.25/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= +github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= +github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.4/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= @@ -323,8 +324,8 @@ github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zK github.com/aws/aws-sdk-go v1.42.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.22/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.25/go.mod h1:gyRszuZ/icHmHAVE4gc/r+cfCmhA1AD+vqfWbgI+eHs= -github.com/aws/aws-sdk-go v1.43.30 h1:Q3lgrX/tz/MkEiPVVQnOQThBAK2QC2SCTCKTD1mwGFA= -github.com/aws/aws-sdk-go v1.43.30/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.43.45 h1:2708Bj4uV+ym62MOtBnErm/CDX61C4mFe9V2gXy1caE= +github.com/aws/aws-sdk-go v1.43.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250= github.com/aws/aws-sdk-go-v2 v1.11.0/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ= @@ -713,8 +714,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1 h1:cgDRLG7bs59Zd+apAWuzLQL95obVYAymNJek76W3mgw= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -948,8 +950,8 @@ github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-rod/rod v0.101.8/go.mod h1:N/zlT53CfSpq74nb6rOR0K8UF0SPUPBmzBnArrms+mY= -github.com/go-rod/rod v0.104.4 h1:sQR35AFo9ceR7ksh+Ld81bQzIbrXlQH/IO46iCWqxts= -github.com/go-rod/rod v0.104.4/go.mod h1:trmrxxg+qUodIIQiYeyJbW5ZMo0FSajmdEGw2tHzlM4= +github.com/go-rod/rod v0.106.1 h1:+9YdoTT56KI3KrFfWVr3I13wh0qbhm/Aq+7JvCBA6AQ= +github.com/go-rod/rod v0.106.1/go.mod h1:+YLe2X+nAuEGpYWs7rKPZr9SMX100FbxYZaeU1Dofpc= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -1052,7 +1054,6 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -1224,8 +1225,9 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0 h1:s7jOdKSaksJVOxE0Y/S32otcfiP+UQ0cL8/GTKaONwE= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= @@ -1324,8 +1326,9 @@ github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -1335,8 +1338,8 @@ github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmk github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 h1:geBw3SBrxQq+buvbf4K+Qltv1gjaXJxy8asD4CjGYow= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4 h1:hrIH/qrOTHfG9a1Jz6Z2jQf7Xe77AaD464W1fCFLwPQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= @@ -1373,6 +1376,7 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= github.com/hashicorp/vault/api v1.5.0 h1:Bp6yc2bn7CWkOrVIzFT/Qurzx528bdavF3nz590eu28= @@ -1582,8 +1586,9 @@ github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXq github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -1691,8 +1696,9 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -1714,7 +1720,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= @@ -1848,6 +1853,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= @@ -1966,8 +1973,9 @@ github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -1990,6 +1998,7 @@ github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiB github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= +github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= @@ -2025,8 +2034,8 @@ github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 h1:mbqXrm8YZXN/cJ github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3/go.mod h1:u9clLqaVjqV9pExVL1XkM37dGyMCOX/LMocS9nsnWDY= github.com/sigstore/sigstore v1.0.2-0.20211210190220-04746d994282/go.mod h1:SuM+QIHtnnR9eGsURRLv5JfxM6KeaU0XKA1O7FmLs4Q= github.com/sigstore/sigstore v1.1.0/go.mod h1:gDpcHw4VwpoL5C6N1Ud1YtBsc+ikRDwDelDlWRyYoE8= -github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f h1:JPD9q1718mub78ILVcTqOZ/q4ECKCQ7JQfUX/q+nEJ4= -github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f/go.mod h1:9wYagRiKz/8KgK/YFPM6FA8WrNjv3Y6rQUQWBLqJXs0= +github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5 h1:8OL06Knchax4CMtdfquC3ASWQPtYMJgyeQImWQPw6XE= +github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5/go.mod h1:OvpZniSE9oRPnW7+mhxljRt2RAQU+TwcnhYbqQsPwPc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -2064,8 +2073,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= -github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= @@ -2097,8 +2106,9 @@ github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= +github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= +github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/spiffe/go-spiffe/v2 v2.0.0 h1:y6N7BZAxgaFZYELyrIdxSMm2e2tWpzgQewUts9h1hfM= github.com/spiffe/go-spiffe/v2 v2.0.0/go.mod h1:TEfgrEcyFhuSuvqohJt6IxENUNeHfndWCCV1EX7UaVk= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= @@ -2220,8 +2230,8 @@ github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898 h1:2Z+iziYPiyWk5hVJ3EYLn/i33Tj5ukytaJA0Th9tbgc= github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898/go.mod h1:cKObXQ6PVFO7bHUd5jpApXvMIt55Ewz7UdMiC05ONxI= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= -github.com/xanzy/go-gitlab v0.62.0 h1:D3WuIK1UJ7JPSiYI077PQaU5dcPEshpimCSP07Do1aQ= -github.com/xanzy/go-gitlab v0.62.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= +github.com/xanzy/go-gitlab v0.64.0 h1:rMgQdW9S1w3qvNAH2LYpFd2xh7KNLk+JWJd7sorNuTc= +github.com/xanzy/go-gitlab v0.64.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= @@ -2245,15 +2255,15 @@ github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mo github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= -github.com/ysmood/goob v0.3.1 h1:qMp5364BGS1DLJVrAqUxTF6KOFt0YDot8GC70u/0jbI= -github.com/ysmood/goob v0.3.1/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/got v0.15.1/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= -github.com/ysmood/got v0.19.1/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/got v0.23.3/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= github.com/ysmood/gotrace v0.2.2/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= -github.com/ysmood/gotrace v0.4.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.6.4/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/gson v0.7.0 h1:oQhY2FQtfy3+bgaNeqopd7NGAB6Me+UpG0n7oO4VDko= -github.com/ysmood/gson v0.7.0/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/gson v0.7.1 h1:zKL2MTGtynxdBdlZjyGsvEOZ7dkxaY5TH6QhAbTgz0Q= +github.com/ysmood/gson v0.7.1/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw= github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= @@ -2291,15 +2301,18 @@ go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1Zt go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.1 h1:v28cktvBq+7vGyJXF8G+rWJmj+1XUmMtqcLnH8hDocM= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.2 h1:tXok5yLlKyuQ/SXSjtqHc4uzNaMqZi2XsoSPr/LlJXI= +go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.1 h1:XIQcHCFSG53bJETYeRJtIxdLv2EWRGxcfzR8lSnTH4E= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.2 h1:4hzqQ6hIb3blLyQ8usCU4h3NghkqcsohEQ3o3VetYxE= +go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v2 v2.305.1 h1:vtxYCKWA9x31w0WJj7DdqsHFNjhkigdAnziDtkZb/l4= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.etcd.io/etcd/client/v2 v2.305.2 h1:ymrVwTkefuqA/rPkSW7/B4ApijbPVefRumkY+stNfS0= +go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= @@ -2470,8 +2483,9 @@ golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -2605,8 +2619,9 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127074510-2fabfed7e28f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2631,8 +2646,9 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2794,8 +2810,9 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 h1:eJv7u3ksNXoLbGSKuv2s/SIO4tJVxc/A+MTpzxDgz/Q= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2959,8 +2976,9 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -3013,8 +3031,9 @@ google.golang.org/api v0.65.0/go.mod h1:ArYhxgGadlWmqO1IqVujw6Cs8IdD33bTmzKo2Sh+ google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0 h1:0AYh/ae6l9TDUvIQrDw5QRpM100P6oHgD+o3dYHMzJg= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3133,8 +3152,11 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf h1:JTjwKJX9erVpsw17w+OIPP7iAgEkN/r8urhWSunEDTs= google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 h1:myaecH64R0bIEDjNORIel4iXubqzaHU1K2z8ajBwWcM= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3175,8 +3197,9 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/examples v0.0.0-20201130180447-c456688b1860/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -3221,8 +3244,9 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= @@ -3299,8 +3323,9 @@ k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/code-generator v0.23.4/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= -k8s.io/code-generator v0.23.5 h1:xn3a6J5pUL49AoH6SPrOFtnB5cvdMl76f/bEY176R3c= k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/code-generator v0.23.6 h1:4J4zL5TU7e96kjGvr5LOFsgR1P9ZU/C6EQeGYcNrFvw= +k8s.io/code-generator v0.23.6/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= diff --git a/pkg/apis/config/image_policies.go b/pkg/apis/config/image_policies.go index 3bce8bc80fc..40a6fa5164c 100644 --- a/pkg/apis/config/image_policies.go +++ b/pkg/apis/config/image_policies.go @@ -77,15 +77,15 @@ func parseEntry(entry string, out interface{}) error { // GetMatchingPolicies returns all matching Policies and their Authorities that // need to be matched for the given Image. -// Returned map contains the name of the CIP as the key, and an array of -// authorities from that Policy that must be validated against. -func (p *ImagePolicyConfig) GetMatchingPolicies(image string) (map[string][]webhookcip.Authority, error) { +// Returned map contains the name of the CIP as the key, and a normalized +// ClusterImagePolicy for it. +func (p *ImagePolicyConfig) GetMatchingPolicies(image string) (map[string]webhookcip.ClusterImagePolicy, error) { if p == nil { return nil, errors.New("config is nil") } var lastError error - ret := map[string][]webhookcip.Authority{} + ret := make(map[string]webhookcip.ClusterImagePolicy) // TODO(vaikas): this is very inefficient, we should have a better // way to go from image to Authorities, but just seeing if this is even @@ -94,13 +94,13 @@ func (p *ImagePolicyConfig) GetMatchingPolicies(image string) (map[string][]webh for _, pattern := range v.Images { if pattern.Glob != "" { if GlobMatch(image, pattern.Glob) { - ret[k] = append(ret[k], v.Authorities...) + ret[k] = v } } else if pattern.Regex != "" { if regex, err := regexp.Compile(pattern.Regex); err != nil { lastError = err } else if regex.MatchString(image) { - ret[k] = append(ret[k], v.Authorities...) + ret[k] = v } } } diff --git a/pkg/apis/config/image_policies_test.go b/pkg/apis/config/image_policies_test.go index 7ecea12521d..0336bb80afe 100644 --- a/pkg/apis/config/image_policies_test.go +++ b/pkg/apis/config/image_policies_test.go @@ -15,7 +15,7 @@ package config import ( - "crypto/ecdsa" + "crypto" "crypto/x509" "encoding/pem" "strings" @@ -51,7 +51,7 @@ func TestGetAuthorities(t *testing.T) { checkGetMatches(t, c, err) matchedPolicy := "cluster-image-policy-0" want := "inlinedata here" - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } // Make sure glob matches 'randomstuff*' @@ -59,22 +59,22 @@ func TestGetAuthorities(t *testing.T) { checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-1" want = "otherinline here" - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } c, err = defaults.GetMatchingPolicies("rando3") checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-2" want = "cacert chilling here" - if got := c[matchedPolicy][0].Keyless.CACert.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Keyless.CACert.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } want = "issuer" - if got := c[matchedPolicy][0].Keyless.Identities[0].Issuer; got != want { + if got := c[matchedPolicy].Authorities[0].Keyless.Identities[0].Issuer; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } want = "subject" - if got := c[matchedPolicy][0].Keyless.Identities[0].Subject; got != want { + if got := c[matchedPolicy].Authorities[0].Keyless.Identities[0].Subject; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } // Make sure regex matches ".*regexstring.*" @@ -82,30 +82,30 @@ func TestGetAuthorities(t *testing.T) { checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-4" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) // Test multiline yaml cert c, err = defaults.GetMatchingPolicies("inlinecert") checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-3" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) // Test multiline cert but json encoded c, err = defaults.GetMatchingPolicies("ghcr.io/example/*") checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-json" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) // Test multiple matches c, err = defaults.GetMatchingPolicies("regexstringtoo") @@ -115,19 +115,48 @@ func TestGetAuthorities(t *testing.T) { } matchedPolicy = "cluster-image-policy-4" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) matchedPolicy = "cluster-image-policy-5" want = "inlinedata here" - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + + // Test attestations + top level policy + c, err = defaults.GetMatchingPolicies("withattestations") + checkGetMatches(t, c, err) + if len(c) != 1 { + t.Errorf("Wanted 1 match, got %d", len(c)) + } + matchedPolicy = "cluster-image-policy-with-policy-attestations" + want = "attestation-0" + if got := c[matchedPolicy].Authorities[0].Name; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + // Both top & authority policy is using cue + want = "cue" + if got := c[matchedPolicy].Policy.Type; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + want = "cip level cue here" + if got := c[matchedPolicy].Policy.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + want = "cue" + if got := c[matchedPolicy].Authorities[0].Attestations[0].Type; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + want = "test-cue-here" + if got := c[matchedPolicy].Authorities[0].Attestations[0].Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } } -func checkGetMatches(t *testing.T, c map[string][]webhookcip.Authority, err error) { +func checkGetMatches(t *testing.T, c map[string]webhookcip.ClusterImagePolicy, err error) { t.Helper() if err != nil { t.Error("GetMatches Failed =", err) @@ -136,14 +165,14 @@ func checkGetMatches(t *testing.T, c map[string][]webhookcip.Authority, err erro t.Error("Wanted a config, got none.") } for _, v := range c { - if v != nil || len(v) > 0 { + if v.Authorities != nil || len(v.Authorities) > 0 { return } } t.Error("Wanted a config and non-zero authorities, got no authorities") } -func checkPublicKey(t *testing.T, gotKey *ecdsa.PublicKey) { +func checkPublicKey(t *testing.T, gotKey crypto.PublicKey) { t.Helper() derBytes, err := x509.MarshalPKIXPublicKey(gotKey) @@ -158,7 +187,6 @@ func checkPublicKey(t *testing.T, gotKey *ecdsa.PublicKey) { // pem.EncodeToMemory has an extra newline at the end got := strings.TrimSuffix(string(pemBytes), "\n") - if got != inlineKeyData { t.Errorf("Did not get what I wanted %s, got %s", inlineKeyData, string(pemBytes)) } diff --git a/pkg/apis/config/testdata/config-image-policies.yaml b/pkg/apis/config/testdata/config-image-policies.yaml index ad7154c3f02..23a0d0f6c79 100644 --- a/pkg/apis/config/testdata/config-image-policies.yaml +++ b/pkg/apis/config/testdata/config-image-policies.yaml @@ -31,21 +31,25 @@ data: images: - glob: rando authorities: - - key: + - name: attestation-0 + key: data: inlinedata here - - key: + - name: attestation-1 + key: kms: whatevs cluster-image-policy-1: | images: - glob: randomstuff* authorities: - - key: + - name: attestation-0 + key: data: otherinline here cluster-image-policy-2: | images: - glob: rando3 authorities: - - keyless: + - name: attestation-0 + keyless: ca-cert: data: cacert chilling here url: http://keylessurl.here @@ -56,7 +60,8 @@ data: images: - glob: inlinecert authorities: - - key: + - name: attestation-0 + key: data: |- -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J @@ -66,7 +71,8 @@ data: images: - regex: .*regexstring.* authorities: - - key: + - name: attestation-0 + key: data: |- -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J @@ -76,7 +82,26 @@ data: images: - regex: .*regexstringtoo.* authorities: - - key: + - name: attestation-0 + key: data: inlinedata here cluster-image-policy-json: "{\"images\":[{\"glob\":\"ghcr.io/example/*\",\"regex\":\"\"}],\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}" - + cluster-image-policy-with-policy-attestations: | + images: + - glob: withattestations + authorities: + - name: attestation-0 + keyless: + ca-cert: + data: cacert chilling here + url: http://keylessurl.here + identities: + - issuer: issuer + subject: subject + attestations: + - predicateType: vuln + type: cue + data: "test-cue-here" + policy: + type: cue + data: "cip level cue here" diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go index 66761c5ddc6..431a68d9516 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go @@ -14,8 +14,20 @@ package v1alpha1 -import "context" +import ( + "context" + "fmt" +) // SetDefaults implements apis.Defaultable -func (*ClusterImagePolicy) SetDefaults(ctx context.Context) { +func (c *ClusterImagePolicy) SetDefaults(ctx context.Context) { + c.Spec.SetDefaults(ctx) +} + +func (spec *ClusterImagePolicySpec) SetDefaults(ctx context.Context) { + for i, authority := range spec.Authorities { + if authority.Name == "" { + spec.Authorities[i].Name = fmt.Sprintf("authority-%d", i) + } + } } diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults_test.go new file mode 100644 index 00000000000..0a1ba8e27bf --- /dev/null +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults_test.go @@ -0,0 +1,63 @@ +// Copyright 2022 The Sigstore 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 v1alpha1 + +import ( + "context" + + "testing" + + "knative.dev/pkg/apis" +) + +func TestNameDefaulting(t *testing.T) { + tests := []struct { + in *ClusterImagePolicy + wantNames []string + }{ + {in: cipWithNames([]string{""}), + wantNames: []string{"authority-0"}, + }, + {in: cipWithNames([]string{"", "vuln-scan"}), + wantNames: []string{"authority-0", "vuln-scan"}, + }, + {in: cipWithNames([]string{"vuln-scan", ""}), + wantNames: []string{"vuln-scan", "authority-1"}, + }, + {in: cipWithNames([]string{"first", "second"}), + wantNames: []string{"first", "second"}, + }} + for _, tc := range tests { + tc.in.SetDefaults(context.TODO()) + if len(tc.in.Spec.Authorities) != len(tc.wantNames) { + t.Fatalf("Mismatch number of wantNames: %d vs authorities: %d", len(tc.wantNames), len(tc.in.Spec.Authorities)) + } + for i, wantName := range tc.wantNames { + if tc.in.Spec.Authorities[i].Name != wantName { + t.Errorf("Wanted name: %s got %s", wantName, tc.in.Spec.Authorities[i].Name) + } + } + } +} + +func cipWithNames(names []string) *ClusterImagePolicy { + cip := &ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{}, + } + for _, name := range names { + cip.Spec.Authorities = append(cip.Spec.Authorities, Authority{Name: name, Keyless: &KeylessRef{URL: &apis.URL{Host: "tests.example.com"}}}) + } + return cip +} diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go index d8f30028da4..bad7c36483b 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go @@ -46,7 +46,7 @@ var ( ) // GetGroupVersionKind implements kmeta.OwnerRefable -func (*ClusterImagePolicy) GetGroupVersionKind() schema.GroupVersionKind { +func (c *ClusterImagePolicy) GetGroupVersionKind() schema.GroupVersionKind { return SchemeGroupVersion.WithKind("ClusterImagePolicy") } @@ -54,6 +54,11 @@ func (*ClusterImagePolicy) GetGroupVersionKind() schema.GroupVersionKind { type ClusterImagePolicySpec struct { Images []ImagePattern `json:"images"` Authorities []Authority `json:"authorities"` + // Policy is an optional policy that can be applied against all the + // successfully validated Authorities. If no authorities pass, this does + // not even get evaluated, as the Policy is considered failed. + // +optional + Policy *Policy `json:"policy,omitempty"` } // ImagePattern defines a pattern and its associated authorties @@ -75,6 +80,11 @@ type ImagePattern struct { // image. type Authority struct { + // Name is the name for this authority. Used by the CIP Policy + // validator to be able to reference matching signature or attestation + // verifications. + // If not specified, the name will be authority- + Name string `json:"name"` // +optional Key *KeyRef `json:"key,omitempty"` // +optional @@ -83,6 +93,8 @@ type Authority struct { Sources []Source `json:"source,omitempty"` // +optional CTLog *TLog `json:"ctlog,omitempty"` + // +optional + Attestations []Attestation `json:"attestations,omitempty"` } // This references a public verification key stored in @@ -124,6 +136,45 @@ type KeylessRef struct { CACert *KeyRef `json:"ca-cert,omitempty"` } +// Attestation defines the type of attestation to validate and optionally +// apply a policy decision to it. Authority block is used to verify the +// specified attestation types, and if Policy is specified, then it's applied +// only after the validation of the Attestation signature has been verified. +type Attestation struct { + // Name of the attestation. These can then be referenced at the CIP level + // policy. + Name string `json:"name"` + // Which predicate type to verify. Matches cosign verify-attestation options. + PredicateType string `json:"predicateType"` + // +optional + Policy *Policy `json:"policy,omitempty"` +} + +// Policy specifies a policy to use for Attestation validation. +// Exactly one of Data, URL, or ConfigMapReference must be specified. +type Policy struct { + // Which kind of policy this is, currently only rego or cue are supported. + // Furthermore, only cue is tested :) + Type string `json:"type"` + // +optional + Data string `json:"data,omitempty"` + // +optional + URL *apis.URL `json:"url,omitempty"` + // +optional + ConfigMapRef *ConfigMapReference `json:"configMapRef,omitempty"` +} + +// ConfigMapReference is cut&paste from SecretReference, but for the life of me +// couldn't find one in the public types. If there's one, use it. +type ConfigMapReference struct { + // Name is unique within a namespace to reference a configmap resource. + // +optional + Name string `json:"name,omitempty"` + // Namespace defines the space within which the configmap name must be unique. + // +optional + Namespace string `json:"namespace,omitempty"` +} + // Identity may contain the issuer and/or the subject found in the transparency log. // Either field supports a pattern glob. type Identity struct { diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index 0c8300fbd13..a307f33a834 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go @@ -25,8 +25,8 @@ import ( ) // Validate implements apis.Validatable -func (policy *ClusterImagePolicy) Validate(ctx context.Context) *apis.FieldError { - return policy.Spec.Validate(ctx).ViaField("spec") +func (c *ClusterImagePolicy) Validate(ctx context.Context) *apis.FieldError { + return c.Spec.Validate(ctx).ViaField("spec") } func (spec *ClusterImagePolicySpec) Validate(ctx context.Context) (errors *apis.FieldError) { @@ -42,6 +42,7 @@ func (spec *ClusterImagePolicySpec) Validate(ctx context.Context) (errors *apis. for i, authority := range spec.Authorities { errors = errors.Also(authority.Validate(ctx).ViaFieldIndex("authorities", i)) } + errors = errors.Also(spec.Policy.Validate(ctx)) return } @@ -83,6 +84,14 @@ func (authority *Authority) Validate(ctx context.Context) *apis.FieldError { errs = errs.Also(authority.Keyless.Validate(ctx).ViaField("keyless")) } + for _, source := range authority.Sources { + errs = errs.Also(source.Validate(ctx).ViaField("source")) + } + + for _, att := range authority.Attestations { + errs = errs.Also(att.Validate(ctx).ViaField("attestations")) + } + return errs } @@ -115,12 +124,9 @@ func (keyless *KeylessRef) Validate(ctx context.Context) *apis.FieldError { errs = errs.Also(apis.ErrMissingOneOf("url", "identities", "ca-cert")) } - if keyless.URL != nil { - if keyless.CACert != nil || keyless.Identities != nil { - errs = errs.Also(apis.ErrMultipleOneOf("url", "identities", "ca-cert")) - } - } else if keyless.CACert != nil && keyless.Identities != nil { - errs = errs.Also(apis.ErrMultipleOneOf("url", "identities", "ca-cert")) + // TODO: Are these really mutually exclusive? + if keyless.URL != nil && keyless.CACert != nil { + errs = errs.Also(apis.ErrMultipleOneOf("url", "ca-cert")) } if keyless.Identities != nil && len(keyless.Identities) == 0 { @@ -133,11 +139,58 @@ func (keyless *KeylessRef) Validate(ctx context.Context) *apis.FieldError { return errs } +func (source *Source) Validate(ctx context.Context) *apis.FieldError { + var errs *apis.FieldError + if source.OCI == "" { + errs = errs.Also(apis.ErrMissingField("oci")) + } + return errs +} + +func (a *Attestation) Validate(ctx context.Context) *apis.FieldError { + var errs *apis.FieldError + if a.Name == "" { + errs = errs.Also(apis.ErrMissingField("name")) + } + if a.PredicateType == "" { + errs = errs.Also(apis.ErrMissingField("predicateType")) + } else if a.PredicateType != "custom" && a.PredicateType != "slsaprovenance" && a.PredicateType != "spdx" && a.PredicateType != "link" && a.PredicateType != "vuln" { + // TODO(vaikas): The above should be using something like: + // if _, ok := options.PredicateTypeMap[a.PrecicateType]; !ok { + // But it causes an import loop. That refactor can be part of + // another PR. + errs = errs.Also(apis.ErrInvalidValue(a.PredicateType, "predicateType", "unsupported precicate type")) + } + errs = errs.Also(a.Policy.Validate(ctx).ViaField("policy")) + return errs +} + +func (p *Policy) Validate(ctx context.Context) *apis.FieldError { + if p == nil { + return nil + } + var errs *apis.FieldError + if p.Type != "cue" { + errs = errs.Also(apis.ErrInvalidValue(p.Type, "type", "only cue is supported at the moment")) + } + if p.Data == "" { + errs = errs.Also(apis.ErrMissingField("data")) + } + // TODO(vaikas): How to validate the cue / rego bytes here (data). + return errs +} + func (identity *Identity) Validate(ctx context.Context) *apis.FieldError { var errs *apis.FieldError if identity.Issuer == "" && identity.Subject == "" { errs = errs.Also(apis.ErrMissingOneOf("issuer", "subject")) } + if identity.Issuer != "" { + errs = errs.Also(ValidateRegex(identity.Issuer).ViaField("issuer")) + } + if identity.Subject != "" { + errs = errs.Also(ValidateRegex(identity.Subject).ViaField("subject")) + } return errs } diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go index ba7edfd8645..9848f0a15e5 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go @@ -295,7 +295,7 @@ func TestKeylessValidation(t *testing.T) { { name: "Should fail when keyless has multiple properties", expectErr: true, - errorString: "expected exactly one, got both: spec.authorities[0].keyless.ca-cert, spec.authorities[0].keyless.identities, spec.authorities[0].keyless.url", + errorString: "expected exactly one, got both: spec.authorities[0].keyless.ca-cert, spec.authorities[0].keyless.url", policy: ClusterImagePolicy{ Spec: ClusterImagePolicySpec{ Images: []ImagePattern{ @@ -397,7 +397,97 @@ func TestAuthoritiesValidation(t *testing.T) { }, }, }, + { + name: "Should pass when source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{{OCI: "registry.example.com"}}, + }, + }, + }, + }, + }, + { + name: "Should fail when source oci is empty", + expectErr: true, + errorString: "missing field(s): spec.authorities[0].source.oci", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{{OCI: ""}}, + }, + }, + }, + }, + }, + { + name: "Should pass with multiple source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{ + {OCI: "registry1"}, + {OCI: "registry2"}, + }, + }, + }, + }, + }, + }, + { + name: "Should pass with multiple source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{ + {OCI: "registry1"}, + {OCI: "registry2"}, + }, + }, + }, + }, + }, + }, + { + name: "Should pass with attestations present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Attestations: []Attestation{ + {Name: "first", PredicateType: "vuln"}, + {Name: "second", PredicateType: "custom", Policy: &Policy{ + Type: "cue", + Data: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + }, + }, + }, + }, + }, + }, + }, + }, } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := test.policy.Validate(context.TODO()) @@ -411,6 +501,72 @@ func TestAuthoritiesValidation(t *testing.T) { } } +func TestAttestationsValidation(t *testing.T) { + tests := []struct { + name string + expectErr bool + errorString string + attestation Attestation + }{{ + name: "vuln", + attestation: Attestation{Name: "first", PredicateType: "vuln"}, + }, { + name: "missing name", + attestation: Attestation{PredicateType: "vuln"}, + expectErr: true, + errorString: "missing field(s): name", + }, { + name: "missing predicatetype", + attestation: Attestation{Name: "first"}, + expectErr: true, + errorString: "missing field(s): predicateType", + }, { + name: "invalid predicatetype", + attestation: Attestation{Name: "first", PredicateType: "notsupported"}, + expectErr: true, + errorString: "invalid value: notsupported: predicateType\nunsupported precicate type", + }, { + name: "custom with invalid policy type", + attestation: Attestation{Name: "second", PredicateType: "custom", + Policy: &Policy{ + Type: "not-cue", + Data: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + }, + }, + expectErr: true, + errorString: "invalid value: not-cue: policy.type\nonly cue is supported at the moment", + }, { + name: "custom with missing policy data", + attestation: Attestation{Name: "second", PredicateType: "custom", + Policy: &Policy{ + Type: "cue", + }, + }, + expectErr: true, + errorString: "missing field(s): policy.data", + }, { + name: "custom with policy", + attestation: Attestation{Name: "second", PredicateType: "custom", + Policy: &Policy{ + Type: "cue", + Data: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.attestation.Validate(context.TODO()) + if test.expectErr { + require.NotNil(t, err) + require.EqualError(t, err, test.errorString) + } else { + require.Nil(t, err) + } + }) + } +} func TestIdentitiesValidation(t *testing.T) { tests := []struct { name string @@ -439,6 +595,67 @@ func TestIdentitiesValidation(t *testing.T) { }, }, }, + { + name: "Should fail when issuer has invalid regex", + expectErr: true, + errorString: "invalid value: ****: spec.authorities[0].keyless.identities[0].issuer\nregex is invalid: error parsing regexp: missing argument to repetition operator: `*`", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{ + { + Glob: "globbityglob", + }, + }, + Authorities: []Authority{ + { + Keyless: &KeylessRef{ + Identities: []Identity{{Issuer: "****"}}, + }, + }, + }, + }, + }, + }, + { + name: "Should fail when subject has invalid regex", + expectErr: true, + errorString: "invalid value: ****: spec.authorities[0].keyless.identities[0].subject\nregex is invalid: error parsing regexp: missing argument to repetition operator: `*`", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{ + { + Glob: "globbityglob", + }, + }, + Authorities: []Authority{ + { + Keyless: &KeylessRef{ + Identities: []Identity{{Subject: "****"}}, + }, + }, + }, + }, + }, + }, + { + name: "Should pass when subject and issuer have valid regex", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{ + { + Glob: "globbityglob", + }, + }, + Authorities: []Authority{ + { + Keyless: &KeylessRef{ + Identities: []Identity{{Subject: ".*subject.*", Issuer: ".*issuer.*"}}, + }, + }, + }, + }, + }, + }, { name: "Should pass when identities is valid", expectErr: false, diff --git a/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go index 2c1c886712c..c6926846691 100644 --- a/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,27 @@ import ( apis "knative.dev/pkg/apis" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Attestation) DeepCopyInto(out *Attestation) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(Policy) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Attestation. +func (in *Attestation) DeepCopy() *Attestation { + if in == nil { + return nil + } + out := new(Attestation) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Authority) DeepCopyInto(out *Authority) { *out = *in @@ -48,6 +69,13 @@ func (in *Authority) DeepCopyInto(out *Authority) { *out = new(TLog) (*in).DeepCopyInto(*out) } + if in.Attestations != nil { + in, out := &in.Attestations, &out.Attestations + *out = make([]Attestation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -136,6 +164,11 @@ func (in *ClusterImagePolicySpec) DeepCopyInto(out *ClusterImagePolicySpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(Policy) + (*in).DeepCopyInto(*out) + } return } @@ -149,6 +182,22 @@ func (in *ClusterImagePolicySpec) DeepCopy() *ClusterImagePolicySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMapReference) DeepCopyInto(out *ConfigMapReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapReference. +func (in *ConfigMapReference) DeepCopy() *ConfigMapReference { + if in == nil { + return nil + } + out := new(ConfigMapReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Identity) DeepCopyInto(out *Identity) { *out = *in @@ -233,6 +282,32 @@ func (in *KeylessRef) DeepCopy() *KeylessRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Policy) DeepCopyInto(out *Policy) { + *out = *in + if in.URL != nil { + in, out := &in.URL, &out.URL + *out = new(apis.URL) + (*in).DeepCopyInto(*out) + } + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(ConfigMapReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. +func (in *Policy) DeepCopy() *Policy { + if in == nil { + return nil + } + out := new(Policy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Source) DeepCopyInto(out *Source) { *out = *in diff --git a/pkg/blob/load.go b/pkg/blob/load.go index 9c93fccc779..c92d49c4ca3 100644 --- a/pkg/blob/load.go +++ b/pkg/blob/load.go @@ -15,6 +15,7 @@ package blob import ( + "fmt" "io" "net/http" "os" @@ -25,16 +26,32 @@ import ( func LoadFileOrURL(fileRef string) ([]byte, error) { var raw []byte var err error - if strings.HasPrefix(fileRef, "http://") || strings.HasPrefix(fileRef, "https://") { - // #nosec G107 - resp, err := http.Get(fileRef) - if err != nil { - return nil, err - } - defer resp.Body.Close() - raw, err = io.ReadAll(resp.Body) - if err != nil { - return nil, err + parts := strings.SplitAfterN(fileRef, "://", 2) + if len(parts) == 2 { + scheme := parts[0] + switch scheme { + case "http://": + fallthrough + case "https://": + // #nosec G107 + resp, err := http.Get(fileRef) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + case "env://": + envVar := parts[1] + value, found := os.LookupEnv(envVar) + if !found { + return nil, fmt.Errorf("loading URL: env var $%s not found", envVar) + } + raw = []byte(value) + default: + return nil, fmt.Errorf("loading URL: unrecognized scheme: %s", scheme) } } else { raw, err = os.ReadFile(filepath.Clean(fileRef)) diff --git a/pkg/blob/load_test.go b/pkg/blob/load_test.go new file mode 100644 index 00000000000..add9db7bd62 --- /dev/null +++ b/pkg/blob/load_test.go @@ -0,0 +1,99 @@ +// +// Copyright 2021 The Sigstore 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 blob + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "path" + "runtime" + "testing" +) + +func TestLoadFile(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping on Windows due to https://github.com/golang/go/issues/51442") + } + temp := t.TempDir() + fname := "filename.txt" + path := path.Join(temp, fname) + data := []byte("test") + defer os.Remove(path) + os.WriteFile(path, data, 0400) + + // absolute path + actual, err := LoadFileOrURL(path) + if err != nil { + t.Errorf("Reading from absolute path %s failed: %v", path, err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(absolute path) = '%s'; want '%s'", actual, data) + } + + if err = os.Chdir(temp); err != nil { + t.Fatalf("Chdir('%s'): %v", temp, err) + } + actual, err = LoadFileOrURL(fname) + if err != nil { + t.Errorf("Reading from relative path %s failed: %v", fname, err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(relative path) = '%s'; want '%s'", actual, data) + } +} + +func TestLoadURL(t *testing.T) { + data := []byte("test") + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write(data) + })) + defer server.Close() + + actual, err := LoadFileOrURL(server.URL) + if err != nil { + t.Errorf("Reading from HTTP failed: %v", err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(HTTP) = '%s'; want '%s'", actual, data) + } + + os.Setenv("MY_ENV_VAR", string(data)) + actual, err = LoadFileOrURL("env://MY_ENV_VAR") + if err != nil { + t.Errorf("Reading from environment failed: %v", err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(env) = '%s'; want '%s'", actual, data) + } + + os.Setenv("MY_ENV_VAR", "") + actual, err = LoadFileOrURL("env://MY_ENV_VAR") + if err != nil { + t.Errorf("Reading from environment failed: %v", err) + } else if !bytes.Equal(actual, make([]byte, 0)) { + t.Errorf("LoadFileOrURL(env) = '%s'; should be empty", actual) + } + + os.Unsetenv("MY_ENV_VAR") + _, err = LoadFileOrURL("env://MY_ENV_VAR") + if err == nil { + t.Error("LoadFileOrURL(): expected error for unset env var") + } + + _, err = LoadFileOrURL("invalid://url") + if err == nil { + t.Error("LoadFileOrURL(): expected error for invalid scheme") + } +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index 463dca6b1a1..9b3e1a1f1c0 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -59,6 +59,10 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { diff --git a/pkg/cosign/attestation/attestation.go b/pkg/cosign/attestation/attestation.go index ce113220695..3215c32ca87 100644 --- a/pkg/cosign/attestation/attestation.go +++ b/pkg/cosign/attestation/attestation.go @@ -50,6 +50,15 @@ type CosignVulnPredicate struct { Metadata Metadata `json:"metadata"` } +// I think this will be moving to upstream in-toto in the fullness of time +// but creating it here for now so that we have a way to deserialize it +// as a InToto Statement +// https://github.com/in-toto/attestation/issues/58 +type CosignVulnStatement struct { + in_toto.StatementHeader + Predicate CosignVulnPredicate `json:"predicate"` +} + type Invocation struct { Parameters interface{} `json:"parameters"` URI string `json:"uri"` diff --git a/pkg/cosign/cue/cue_test.go b/pkg/cosign/cue/cue_test.go new file mode 100644 index 00000000000..3feeca92d9c --- /dev/null +++ b/pkg/cosign/cue/cue_test.go @@ -0,0 +1,182 @@ +// +// Copyright 2021 The Sigstore 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 cue + +import ( + "fmt" + "os" + + "testing" +) + +var cueJSONAttestationsBody = ` +{ + "authorityMatches": { + "keyatt": { + "signatures": null, + "attestations": { + "vuln-key": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keysignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + }, + "keylessatt": { + "signatures": null, + "attestations": { + "key1": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "key2": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keylesssignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + } + } + } +` + +var cueJSONSampleBody = `{ + "seq": [ + 1, 2, 3, { + "a": 1, + "b": 2 + } + ], + "a": {"b": {"c": 3}}, + "b": { + "x": 0, + "y": 1, + "z": 2 + } +}` + +func TestValidationJSON(t *testing.T) { + cases := []struct { + name string + jsonBody string + policy string + pass bool + errorMsg string + }{ + { + name: "passing policy", + jsonBody: cueJSONSampleBody, + policy: ` + package test + + seq: [ + 1, 2, 3, { + a: 1 + b: 2 + } + ] + a: b: c: 3 + b: { + x: 0 + y: 1 + z: 2 + } + `, + pass: true, + }, + { + name: "passing result due to matching rules", + jsonBody: cueJSONAttestationsBody, + policy: ` + package test + import "struct" + import "list" + + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + keylessatt: { + attestations: struct.MinFields(2) & struct.MaxFields(2) + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + } + `, + pass: true, + }, + { + name: "policy query evaluates to false signatures array min items", + jsonBody: cueJSONAttestationsBody, + policy: ` + package test + import "list" + + authorityMatches: { + keysignature: { + signatures: list.MaxItems(2) & list.MinItems(2) + } + } + `, + pass: false, + errorMsg: "authorityMatches.keysignature.signatures: invalid value [{subject:\"PLACEHOLDER\",issuer:\"PLACEHOLDER\"}] (does not satisfy list.MinItems(2))", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + policyFileName := "tmp-policy.cue" + if err := os.WriteFile(policyFileName, []byte(tt.policy), 0644); err != nil { + t.Fatal(err) + } + defer os.Remove(policyFileName) + + if err := ValidateJSON([]byte(tt.jsonBody), []string{policyFileName}); (err == nil) != tt.pass { + t.Fatalf("Unexpected result: %v", err) + } else if err != nil { + if fmt.Sprintf("%s", err) != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err) + } + } + }) + } +} diff --git a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go index 9ac247c04c8..cbda0c7c0fb 100644 --- a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go +++ b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go @@ -15,12 +15,17 @@ package clusterimagepolicy import ( - "crypto/ecdsa" + "crypto" "crypto/x509" "encoding/json" "encoding/pem" + "github.com/google/go-containerregistry/pkg/name" + "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + "github.com/sigstore/cosign/pkg/oci/remote" + + "knative.dev/pkg/apis" ) // ClusterImagePolicy defines the images that go through verification @@ -31,17 +36,31 @@ import ( type ClusterImagePolicy struct { Images []v1alpha1.ImagePattern `json:"images"` Authorities []Authority `json:"authorities"` + // Policy is an optional policy used to evaluate the results of valid + // Authorities. Will not get evaluated unless at least one Authority + // succeeds. + Policy *AttestationPolicy `json:"policy,omitempty"` } type Authority struct { + // Name is the name for this authority. Used by the CIP Policy + // validator to be able to reference matching signature or attestation + // verifications. + Name string `json:"name"` // +optional Key *KeyRef `json:"key,omitempty"` // +optional - Keyless *v1alpha1.KeylessRef `json:"keyless,omitempty"` + Keyless *KeylessRef `json:"keyless,omitempty"` // +optional Sources []v1alpha1.Source `json:"source,omitempty"` // +optional CTLog *v1alpha1.TLog `json:"ctlog,omitempty"` + // RemoteOpts are not marshalled because they are an unsupported type + // RemoteOpts will be populated by the Authority UnmarshalJSON override + // +optional + RemoteOpts []remote.Option `json:"-"` + // +optional + Attestations []AttestationPolicy `json:"attestations,omitempty"` } // This references a public verification key stored in @@ -50,19 +69,37 @@ type KeyRef struct { // Data contains the inline public key // +optional Data string `json:"data,omitempty"` - // KMS contains the KMS url of the public key - // +optional - KMS string `json:"kms,omitempty"` // PublicKeys are not marshalled because JSON unmarshalling // errors for *big.Int // +optional - PublicKeys []*ecdsa.PublicKey `json:"-"` + PublicKeys []crypto.PublicKey `json:"-"` +} + +type KeylessRef struct { + // +optional + URL *apis.URL `json:"url,omitempty"` + // +optional + Identities []v1alpha1.Identity `json:"identities,omitempty"` + // +optional + CACert *KeyRef `json:"ca-cert,omitempty"` +} + +type AttestationPolicy struct { + // Name of the Attestation + Name string `json:"name"` + // PredicateType to attest, one of the accepted in verify-attestation + PredicateType string `json:"predicateType"` + // Type specifies how to evaluate policy, only rego/cue are understood. + Type string `json:"type,omitempty"` + // Data is the inlined version of the Policy used to evaluate the + // Attestation. + Data string `json:"data,omitempty"` } // UnmarshalJSON populates the PublicKeys using Data because // JSON unmashalling errors for *big.Int func (k *KeyRef) UnmarshalJSON(data []byte) error { - var publicKeys []*ecdsa.PublicKey + var publicKeys []crypto.PublicKey var err error ret := make(map[string]string) @@ -73,7 +110,7 @@ func (k *KeyRef) UnmarshalJSON(data []byte) error { k.Data = ret["data"] if ret["data"] != "" { - publicKeys, err = convertKeyDataToPublicKeys(ret["data"]) + publicKeys, err = ConvertKeyDataToPublicKeys(ret["data"]) if err != nil { return err } @@ -84,29 +121,143 @@ func (k *KeyRef) UnmarshalJSON(data []byte) error { return nil } -func convertKeyDataToPublicKeys(pubKey string) ([]*ecdsa.PublicKey, error) { - keys := []*ecdsa.PublicKey{} +// UnmarshalJSON populates the authority with the remoteOpts +// from authority sources +func (a *Authority) UnmarshalJSON(data []byte) error { + // Create a new type to avoid recursion + type RawAuthority Authority - pems := parsePems([]byte(pubKey)) - for _, p := range pems { - key, err := x509.ParsePKIXPublicKey(p.Bytes) - if err != nil { - return nil, err + var rawAuthority RawAuthority + err := json.Unmarshal(data, &rawAuthority) + if err != nil { + return err + } + + // Determine additional RemoteOpts + if len(rawAuthority.Sources) > 0 { + for _, source := range rawAuthority.Sources { + if targetRepoOverride, err := name.NewRepository(source.OCI); err != nil { + return errors.Wrap(err, "failed to determine source") + } else if (targetRepoOverride != name.Repository{}) { + rawAuthority.RemoteOpts = append(rawAuthority.RemoteOpts, remote.WithTargetRepository(targetRepoOverride)) + } } - keys = append(keys, key.(*ecdsa.PublicKey)) } - return keys, nil + + // Set the new type instance to casted original + *a = Authority(rawAuthority) + return nil +} + +func ConvertClusterImagePolicyV1alpha1ToWebhook(in *v1alpha1.ClusterImagePolicy) *ClusterImagePolicy { + copyIn := in.DeepCopy() + + outAuthorities := make([]Authority, 0) + for _, authority := range copyIn.Spec.Authorities { + outAuthority := convertAuthorityV1Alpha1ToWebhook(authority) + outAuthorities = append(outAuthorities, *outAuthority) + } + + // If there's a ClusterImagePolicy level AttestationPolicy, convert it here. + var cipAttestationPolicy *AttestationPolicy + if in.Spec.Policy != nil { + cipAttestationPolicy = &AttestationPolicy{ + Type: in.Spec.Policy.Type, + Data: in.Spec.Policy.Data, + } + } + return &ClusterImagePolicy{ + Images: copyIn.Spec.Images, + Authorities: outAuthorities, + Policy: cipAttestationPolicy, + } } -func parsePems(b []byte) []*pem.Block { - p, rest := pem.Decode(b) - if p == nil { +func convertAuthorityV1Alpha1ToWebhook(in v1alpha1.Authority) *Authority { + keyRef := convertKeyRefV1Alpha1ToWebhook(in.Key) + keylessRef := convertKeylessRefV1Alpha1ToWebhook(in.Keyless) + attestations := convertAttestationsV1Alpha1ToWebhook(in.Attestations) + + return &Authority{ + Name: in.Name, + Key: keyRef, + Keyless: keylessRef, + Sources: in.Sources, + CTLog: in.CTLog, + Attestations: attestations, + } +} + +func convertAttestationsV1Alpha1ToWebhook(in []v1alpha1.Attestation) []AttestationPolicy { + ret := []AttestationPolicy{} + for _, inAtt := range in { + outAtt := AttestationPolicy{ + Name: inAtt.Name, + PredicateType: inAtt.PredicateType, + } + if inAtt.Policy != nil { + outAtt.Type = inAtt.Policy.Type + outAtt.Data = inAtt.Policy.Data + } + ret = append(ret, outAtt) + } + return ret +} + +func convertKeyRefV1Alpha1ToWebhook(in *v1alpha1.KeyRef) *KeyRef { + if in == nil { + return nil + } + + return &KeyRef{ + Data: in.Data, + } +} + +func convertKeylessRefV1Alpha1ToWebhook(in *v1alpha1.KeylessRef) *KeylessRef { + if in == nil { return nil } - pems := []*pem.Block{p} - if rest != nil { - return append(pems, parsePems(rest)...) + CACertRef := convertKeyRefV1Alpha1ToWebhook(in.CACert) + + return &KeylessRef{ + URL: in.URL, + Identities: in.Identities, + CACert: CACertRef, } - return pems +} + +func parsePEMKey(b []byte) ([]*pem.Block, bool) { + pemKey, rest := pem.Decode(b) + valid := true + if pemKey == nil { + return nil, false + } + pemBlocks := []*pem.Block{pemKey} + + if len(rest) > 0 { + list, check := parsePEMKey(rest) + return append(pemBlocks, list...), check + } + return pemBlocks, valid +} + +func ConvertKeyDataToPublicKeys(pubKey string) ([]crypto.PublicKey, error) { + keys := []crypto.PublicKey{} + pems, validPEM := parsePEMKey([]byte(pubKey)) + if !validPEM { + // TODO: If it is not valid report the error instead of ignore the key + return keys, nil + } + + for _, p := range pems { + key, err := x509.ParsePKIXPublicKey(p.Bytes) + if err != nil { + return nil, err + } + keys = append(keys, key.(crypto.PublicKey)) + } + + return keys, nil } diff --git a/pkg/cosign/kubernetes/webhook/validation.go b/pkg/cosign/kubernetes/webhook/validation.go index 72dc704341e..278cc5439fc 100644 --- a/pkg/cosign/kubernetes/webhook/validation.go +++ b/pkg/cosign/kubernetes/webhook/validation.go @@ -18,7 +18,6 @@ package webhook import ( "context" "crypto" - "crypto/ecdsa" "crypto/x509" "encoding/pem" "errors" @@ -29,6 +28,7 @@ import ( "knative.dev/pkg/logging" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioroots" + v1alpha1 "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/oci" ociremote "github.com/sigstore/cosign/pkg/oci/remote" @@ -36,10 +36,10 @@ import ( "github.com/sigstore/sigstore/pkg/signature" ) -func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opts ...ociremote.Option) ([]oci.Signature, error) { +func valid(ctx context.Context, ref name.Reference, rekorClient *client.Rekor, keys []crypto.PublicKey, opts ...ociremote.Option) ([]oci.Signature, error) { if len(keys) == 0 { // If there are no keys, then verify against the fulcio root. - sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroots.Get(), nil /* rekor */, opts...) + sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroots.Get(), nil /* rekor */, nil /* no identities */, opts...) if err != nil { return nil, err } @@ -51,14 +51,14 @@ func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opt // We return nil if ANY key matches var lastErr error for _, k := range keys { - verifier, err := signature.LoadECDSAVerifier(k, crypto.SHA256) + verifier, err := signature.LoadVerifier(k, crypto.SHA256) if err != nil { logging.FromContext(ctx).Errorf("error creating verifier: %v", err) lastErr = err continue } - sps, err := validSignatures(ctx, ref, verifier, opts...) + sps, err := validSignatures(ctx, ref, verifier, rekorClient, opts...) if err != nil { logging.FromContext(ctx).Errorf("error validating signatures: %v", err) lastErr = err @@ -74,11 +74,13 @@ func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opt // For testing var cosignVerifySignatures = cosign.VerifyImageSignatures +var cosignVerifyAttestations = cosign.VerifyImageAttestations -func validSignatures(ctx context.Context, ref name.Reference, verifier signature.Verifier, opts ...ociremote.Option) ([]oci.Signature, error) { +func validSignatures(ctx context.Context, ref name.Reference, verifier signature.Verifier, rekorClient *client.Rekor, opts ...ociremote.Option) ([]oci.Signature, error) { sigs, _, err := cosignVerifySignatures(ctx, ref, &cosign.CheckOpts{ RegistryClientOpts: opts, SigVerifier: verifier, + RekorClient: rekorClient, ClaimVerifier: cosign.SimpleClaimVerifier, }) return sigs, err @@ -86,18 +88,51 @@ func validSignatures(ctx context.Context, ref name.Reference, verifier signature // validSignaturesWithFulcio expects a Fulcio Cert to verify against. An // optional rekorClient can also be given, if nil passed, default is assumed. -func validSignaturesWithFulcio(ctx context.Context, ref name.Reference, fulcioRoots *x509.CertPool, rekorClient *client.Rekor, opts ...ociremote.Option) ([]oci.Signature, error) { +func validSignaturesWithFulcio(ctx context.Context, ref name.Reference, fulcioRoots *x509.CertPool, rekorClient *client.Rekor, identities []v1alpha1.Identity, opts ...ociremote.Option) ([]oci.Signature, error) { + ids := make([]cosign.Identity, len(identities)) + for i, id := range identities { + ids[i] = cosign.Identity{Issuer: id.Issuer, Subject: id.Subject} + } sigs, _, err := cosignVerifySignatures(ctx, ref, &cosign.CheckOpts{ RegistryClientOpts: opts, RootCerts: fulcioRoots, RekorClient: rekorClient, ClaimVerifier: cosign.SimpleClaimVerifier, + Identities: ids, }) return sigs, err } -func getKeys(ctx context.Context, cfg map[string][]byte) ([]*ecdsa.PublicKey, *apis.FieldError) { - keys := []*ecdsa.PublicKey{} +func validAttestations(ctx context.Context, ref name.Reference, verifier signature.Verifier, rekorClient *client.Rekor, opts ...ociremote.Option) ([]oci.Signature, error) { + attestations, _, err := cosignVerifyAttestations(ctx, ref, &cosign.CheckOpts{ + RegistryClientOpts: opts, + SigVerifier: verifier, + RekorClient: rekorClient, + ClaimVerifier: cosign.IntotoSubjectClaimVerifier, + }) + return attestations, err +} + +// validAttestationsWithFulcio expects a Fulcio Cert to verify against. An +// optional rekorClient can also be given, if nil passed, default is assumed. +func validAttestationsWithFulcio(ctx context.Context, ref name.Reference, fulcioRoots *x509.CertPool, rekorClient *client.Rekor, identities []v1alpha1.Identity, opts ...ociremote.Option) ([]oci.Signature, error) { + ids := make([]cosign.Identity, len(identities)) + for i, id := range identities { + ids[i] = cosign.Identity{Issuer: id.Issuer, Subject: id.Subject} + } + + attestations, _, err := cosignVerifyAttestations(ctx, ref, &cosign.CheckOpts{ + RegistryClientOpts: opts, + RootCerts: fulcioRoots, + RekorClient: rekorClient, + ClaimVerifier: cosign.IntotoSubjectClaimVerifier, + Identities: ids, + }) + return attestations, err +} + +func getKeys(ctx context.Context, cfg map[string][]byte) ([]crypto.PublicKey, *apis.FieldError) { + keys := []crypto.PublicKey{} errs := []error{} logging.FromContext(ctx).Debugf("Got public key: %v", cfg["cosign.pub"]) @@ -109,7 +144,7 @@ func getKeys(ctx context.Context, cfg map[string][]byte) ([]*ecdsa.PublicKey, *a if err != nil { errs = append(errs, err) } else { - keys = append(keys, key.(*ecdsa.PublicKey)) + keys = append(keys, key.(crypto.PublicKey)) } } if keys == nil { diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index 474d81f35a2..54440b133fd 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -17,10 +17,11 @@ package webhook import ( "context" + "crypto" "crypto/x509" + "encoding/json" "fmt" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -29,9 +30,11 @@ import ( webhookcip "github.com/sigstore/cosign/pkg/cosign/kubernetes/webhook/clusterimagepolicy" "github.com/sigstore/cosign/pkg/oci" ociremote "github.com/sigstore/cosign/pkg/oci/remote" + "github.com/sigstore/cosign/pkg/policy" "github.com/sigstore/fulcio/pkg/api" rekor "github.com/sigstore/rekor/pkg/client" "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/sigstore/pkg/signature" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" listersv1 "k8s.io/client-go/listers/core/v1" @@ -168,15 +171,20 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt // If there is at least one policy that matches, that means it // has to be satisfied. if len(policies) > 0 { - signatures, fieldErrors := validatePolicies(ctx, ref, kc, policies) + signatures, fieldErrors := validatePolicies(ctx, ref, policies, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) + if len(signatures) != len(policies) { logging.FromContext(ctx).Warnf("Failed to validate at least one policy for %s", ref.Name()) // Do we really want to add all the error details here? // Seems like we can just say which policy failed, so // doing that for now. - for failingPolicy := range fieldErrors { + for failingPolicy, policyErrs := range fieldErrors { errorField := apis.ErrGeneric(fmt.Sprintf("failed policy: %s", failingPolicy), "image").ViaFieldIndex(field, i) - errorField.Details = c.Image + errDetails := c.Image + for _, policyErr := range policyErrs { + errDetails = errDetails + " " + policyErr.Error() + } + errorField.Details = errDetails errs = errs.Also(errorField) } // Because there was at least one policy that was @@ -203,7 +211,7 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt continue } - if _, err := valid(ctx, ref, containerKeys, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))); err != nil { + if _, err := valid(ctx, ref, nil, containerKeys, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))); err != nil { errorField := apis.ErrGeneric(err.Error(), "image").ViaFieldIndex(field, i) errorField.Details = c.Image errs = errs.Also(errorField) @@ -227,12 +235,14 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt // Note that if an image does not match any policies, it's perfectly // reasonable that the return value is 0, nil since there were no errors, but // the image was not validated against any matching policy and hence authority. -func validatePolicies(ctx context.Context, ref name.Reference, kc authn.Keychain, policies map[string][]webhookcip.Authority, remoteOpts ...ociremote.Option) (map[string][]oci.Signature, map[string][]error) { - // Gather all validated signatures here. - signatures := map[string][]oci.Signature{} - // For a policy that does not pass at least one authority, gather errors - // here so that we can give meaningful errors to the user. - ret := map[string][]error{} +func validatePolicies(ctx context.Context, ref name.Reference, policies map[string]webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (map[string]*PolicyResult, map[string][]error) { + type retChannelType struct { + name string + policyResult *PolicyResult + errors []error + } + results := make(chan retChannelType, len(policies)) + // For each matching policy it must validate at least one Authority within // it. // From the Design document, the part about multiple Policies matching: @@ -240,85 +250,296 @@ func validatePolicies(ctx context.Context, ref name.Reference, kc authn.Keychain // policies must be satisfied for the image to be admitted." // If none of the Authorities for a given policy pass the checks, gather // the errors here. If one passes, do not return the errors. - authorityErrors := []error{} - for p, authorities := range policies { - logging.FromContext(ctx).Debugf("Checking Policy: %s", p) - sigs, errs := ValidatePolicy(ctx, ref, kc, authorities, remoteOpts...) - if len(errs) > 0 { - ret[p] = append(ret[p], authorityErrors...) - } else { - signatures[p] = append(signatures[p], sigs...) + for cipName, cip := range policies { + // Due to running in gofunc + cipName := cipName + cip := cip + logging.FromContext(ctx).Debugf("Checking Policy: %s", cipName) + go func() { + result := retChannelType{name: cipName} + + result.policyResult, result.errors = ValidatePolicy(ctx, ref, cip, remoteOpts...) + if len(result.errors) == 0 { + // Ok, at least one Authority on the policy passed. If there's a CIP level + // policy, apply it against the results of the successful Authorities + // outputs. + if cip.Policy != nil { + logging.FromContext(ctx).Infof("Validating CIP level policy for %s", cipName) + policyJSON, err := json.Marshal(result.policyResult) + if err != nil { + result.errors = append(result.errors, err) + } else { + logging.FromContext(ctx).Debugf("Validating CIP level policy against %s", string(policyJSON)) + err = policy.EvaluatePolicyAgainstJSON(ctx, "ClusterImagePolicy", cip.Policy.Type, cip.Policy.Data, policyJSON) + if err != nil { + result.errors = append(result.errors, err) + } + } + } + } + results <- result + }() + } + // Gather all validated policies here. + policyResults := make(map[string]*PolicyResult) + // For a policy that does not pass at least one authority, gather errors + // here so that we can give meaningful errors to the user. + ret := map[string][]error{} + + for i := 0; i < len(policies); i++ { + select { + case <-ctx.Done(): + ret["internalerror"] = append(ret["internalerror"], fmt.Errorf("context was canceled before validation completed")) + case result, ok := <-results: + if !ok { + ret["internalerror"] = append(ret["internalerror"], fmt.Errorf("results channel failed to produce a result")) + continue + } + switch { + case len(result.errors) > 0: + ret[result.name] = append(ret[result.name], result.errors...) + case len(result.policyResult.AuthorityMatches) > 0: + policyResults[result.name] = result.policyResult + default: + ret[result.name] = append(ret[result.name], fmt.Errorf("failed to process policy: %s", result.name)) + } } } - return signatures, ret + + return policyResults, ret } -// ValidatePolicy will go through all the Authorities for a given image and -// return a success if at least one of the Authorities validated the signatures. -// Returns the validated signatures, or the errors encountered. -func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, authorities []webhookcip.Authority, remoteOpts ...ociremote.Option) ([]oci.Signature, []error) { +// ValidatePolicy will go through all the Authorities for a given image/policy +// and return a success if at least one of the Authorities validated the +// signatures OR attestations if atttestations were specified. +// Returns PolicyResult, or errors encountered if none of the authorities +// passed. +func ValidatePolicy(ctx context.Context, ref name.Reference, cip webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (*PolicyResult, []error) { + // Each gofunc creates and puts one of these into a results channel. + // Once each gofunc finishes, we go through the channel and pull out + // the results. + type retChannelType struct { + name string + attestations map[string][]PolicySignature + signatures []PolicySignature + err error + } + results := make(chan retChannelType, len(cip.Authorities)) + for _, authority := range cip.Authorities { + authority := authority // due to gofunc + logging.FromContext(ctx).Debugf("Checking Authority: %s", authority.Name) + + go func() { + result := retChannelType{name: authority.Name} + // Assignment for appendAssign lint error + authorityRemoteOpts := remoteOpts + authorityRemoteOpts = append(authorityRemoteOpts, authority.RemoteOpts...) + + if len(authority.Attestations) > 0 { + // We're doing the verify-attestations path, so validate (.att) + validatedAttestations, err := ValidatePolicyAttestationsForAuthority(ctx, ref, authority, authorityRemoteOpts...) + if err != nil { + result.err = err + } else { + result.attestations = validatedAttestations + } + } else { + validatedSignatures, err := ValidatePolicySignaturesForAuthority(ctx, ref, authority, authorityRemoteOpts...) + if err != nil { + result.err = err + } else { + result.signatures = validatedSignatures + } + } + results <- result + }() + } // If none of the Authorities for a given policy pass the checks, gather // the errors here. If one passes, do not return the errors. authorityErrors := []error{} - for _, authority := range authorities { - logging.FromContext(ctx).Debugf("Checking Authority: %+v", authority) - // TODO(vaikas): We currently only use the kc, we have to look - // at authority.Sources to determine additional information for the - // WithRemoteOptions below, at least the 'TargetRepository' - // https://github.com/sigstore/cosign/issues/1651 - - switch { - case authority.Key != nil && len(authority.Key.PublicKeys) > 0: - // TODO(vaikas): What should happen if there are multiple keys - // Is it even allowed? 'valid' returns success if any key - // matches. - // https://github.com/sigstore/cosign/issues/1652 - sps, err := valid(ctx, ref, authority.Key.PublicKeys, remoteOpts...) + // We collect all the successfully satisfied Authorities into this and + // return it. + policyResult := &PolicyResult{AuthorityMatches: make(map[string]AuthorityMatch)} + for i := 0; i < len(cip.Authorities); i++ { + select { + case <-ctx.Done(): + authorityErrors = append(authorityErrors, fmt.Errorf("context was canceled before validation completed")) + case result, ok := <-results: + if !ok { + authorityErrors = append(authorityErrors, fmt.Errorf("results channel failed to produce a result")) + continue + } + switch { + case result.err != nil: + authorityErrors = append(authorityErrors, result.err) + case len(result.signatures) > 0: + policyResult.AuthorityMatches[result.name] = AuthorityMatch{Signatures: result.signatures} + case len(result.attestations) > 0: + policyResult.AuthorityMatches[result.name] = AuthorityMatch{Attestations: result.attestations} + default: + authorityErrors = append(authorityErrors, fmt.Errorf("failed to process authority: %s", result.name)) + } + } + } + if len(authorityErrors) > 0 { + return nil, authorityErrors + } + return policyResult, authorityErrors +} + +func ociSignatureToPolicySignature(ctx context.Context, sigs []oci.Signature) []PolicySignature { + // TODO(vaikas): Validate whether these are useful at all, or if we should + // simplify at least for starters. + ret := []PolicySignature{} + for _, ociSig := range sigs { + logging.FromContext(ctx).Debugf("Converting signature %+v", ociSig) + ret = append(ret, PolicySignature{Subject: "PLACEHOLDER", Issuer: "PLACEHOLDER"}) + } + return ret +} + +// ValidatePolicySignaturesForAuthority takes the Authority and tries to +// verify a signature against it. +func ValidatePolicySignaturesForAuthority(ctx context.Context, ref name.Reference, authority webhookcip.Authority, remoteOpts ...ociremote.Option) ([]PolicySignature, error) { + name := authority.Name + + var rekorClient *client.Rekor + var err error + if authority.CTLog != nil && authority.CTLog.URL != nil { + logging.FromContext(ctx).Debugf("Using CTLog %s for %s", authority.CTLog.URL, ref.Name()) + rekorClient, err = rekor.GetRekorClient(authority.CTLog.URL.String()) + if err != nil { + logging.FromContext(ctx).Errorf("failed creating rekor client: +v", err) + return nil, errors.Wrap(err, "creating Rekor client") + } + } + + switch { + case authority.Key != nil && len(authority.Key.PublicKeys) > 0: + // TODO(vaikas): What should happen if there are multiple keys + // Is it even allowed? 'valid' returns success if any key + // matches. + // https://github.com/sigstore/cosign/issues/1652 + sps, err := valid(ctx, ref, rekorClient, authority.Key.PublicKeys, remoteOpts...) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to validate public keys with authority %s for %s", name, ref.Name())) + } else if len(sps) > 0 { + logging.FromContext(ctx).Debugf("validated signature for %s with authority %s got %d signatures", ref.Name(), authority.Name, len(sps)) + return ociSignatureToPolicySignature(ctx, sps), nil + } + logging.FromContext(ctx).Errorf("no validSignatures found with authority %s for %s", name, ref.Name()) + return nil, fmt.Errorf("no valid signatures found with authority %s for %s", name, ref.Name()) + case authority.Keyless != nil: + if authority.Keyless != nil && authority.Keyless.URL != nil { + logging.FromContext(ctx).Debugf("Fetching FulcioRoot for %s : From: %s ", ref.Name(), authority.Keyless.URL) + fulcioroot, err := getFulcioCert(authority.Keyless.URL) + if err != nil { + return nil, errors.Wrap(err, "fetching FulcioRoot") + } + sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, remoteOpts...) + if err != nil { + logging.FromContext(ctx).Errorf("failed validSignatures for authority %s with fulcio for %s: %v", name, ref.Name(), err) + return nil, errors.Wrap(err, "validate signatures with fulcio") + } else if len(sps) > 0 { + logging.FromContext(ctx).Debugf("validated signature for %s, got %d signatures", ref.Name(), len(sps)) + return ociSignatureToPolicySignature(ctx, sps), nil + } + logging.FromContext(ctx).Errorf("no validSignatures found for %s", ref.Name()) + return nil, fmt.Errorf("no valid signatures found with authority %s for %s", name, ref.Name()) + } + } + // This should never happen because authority has to have been + // validated to be either having a Key or Keyless + return nil, fmt.Errorf("authority has neither key or keyless specified") +} + +// ValidatePolicyAttestationsForAuthority takes the Authority and tries to +// verify attestations against it. +func ValidatePolicyAttestationsForAuthority(ctx context.Context, ref name.Reference, authority webhookcip.Authority, remoteOpts ...ociremote.Option) (map[string][]PolicySignature, error) { + name := authority.Name + var rekorClient *client.Rekor + var err error + if authority.CTLog != nil && authority.CTLog.URL != nil { + logging.FromContext(ctx).Debugf("Using CTLog %s for %s", authority.CTLog.URL, ref.Name()) + rekorClient, err = rekor.GetRekorClient(authority.CTLog.URL.String()) + if err != nil { + logging.FromContext(ctx).Errorf("failed creating rekor client: +v", err) + return nil, errors.Wrap(err, "creating Rekor client") + } + } + + verifiedAttestations := []oci.Signature{} + switch { + case authority.Key != nil && len(authority.Key.PublicKeys) > 0: + for _, k := range authority.Key.PublicKeys { + verifier, err := signature.LoadVerifier(k, crypto.SHA256) if err != nil { - authorityErrors = append(authorityErrors, errors.Wrap(err, "failed to validate keys")) + logging.FromContext(ctx).Errorf("error creating verifier: %v", err) + return nil, errors.Wrap(err, "creating verifier") + } + va, err := validAttestations(ctx, ref, verifier, rekorClient, remoteOpts...) + if err != nil { + logging.FromContext(ctx).Errorf("error validating attestations: %v", err) + return nil, errors.Wrap(err, "validating attestations") + } + verifiedAttestations = append(verifiedAttestations, va...) + } + logging.FromContext(ctx).Debug("No valid signatures were found.") + case authority.Keyless != nil: + if authority.Keyless != nil && authority.Keyless.URL != nil { + logging.FromContext(ctx).Debugf("Fetching FulcioRoot for %s : From: %s ", ref.Name(), authority.Keyless.URL) + fulcioroot, err := getFulcioCert(authority.Keyless.URL) + if err != nil { + return nil, errors.Wrap(err, "fetching FulcioRoot") + } + va, err := validAttestationsWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, remoteOpts...) + if err != nil { + logging.FromContext(ctx).Errorf("failed validAttestationsWithFulcio for authority %s with fulcio for %s: %v", name, ref.Name(), err) + return nil, errors.Wrap(err, "validate signatures with fulcio") + } + verifiedAttestations = append(verifiedAttestations, va...) + } + } + // If we didn't get any verified attestations either from the Key or Keyless + // path, then error out + if len(verifiedAttestations) == 0 { + logging.FromContext(ctx).Errorf("no valid attestations found with authority %s for %s", name, ref.Name()) + return nil, fmt.Errorf("no valid attestations found with authority %s for %s", name, ref.Name()) + } + logging.FromContext(ctx).Debugf("Found %d valid attestations, validating policies for them", len(verifiedAttestations)) + // Now spin through the Attestations that the user specified and validate + // them. + // TODO(vaikas): Pretty inefficient here, figure out a better way if + // possible. + ret := map[string][]PolicySignature{} + for _, wantedAttestation := range authority.Attestations { + // If there's no type / policy to do more checking against, + // then we're done here. It matches all the attestations + if wantedAttestation.Type == "" { + ret[wantedAttestation.Name] = ociSignatureToPolicySignature(ctx, verifiedAttestations) + continue + } + // There's a particular type, so we need to go through all the verified + // attestations and make sure that our particular one is satisfied. + for _, va := range verifiedAttestations { + attBytes, err := policy.AttestationToPayloadJSON(ctx, wantedAttestation.PredicateType, va) + if err != nil { + return nil, errors.Wrap(err, "failed to convert attestation payload to json") + } + if attBytes == nil { + // This happens when we ask for a predicate type that this + // attestation is not for. It's not an error, so we skip it. continue - } else { - if len(sps) > 0 { - logging.FromContext(ctx).Debugf("validated signature for %s, got %d signatures", ref.Name(), len(sps)) - return sps, nil - } - logging.FromContext(ctx).Errorf("no validSignatures found for %s", ref.Name()) - authorityErrors = append(authorityErrors, fmt.Errorf("no valid signatures found for %s", ref.Name())) } - case authority.Keyless != nil: - if authority.Keyless != nil && authority.Keyless.URL != nil { - logging.FromContext(ctx).Debugf("Fetching FulcioRoot for %s : From: %s ", ref.Name(), authority.Keyless.URL) - fulcioroot, err := getFulcioCert(authority.Keyless.URL) - if err != nil { - authorityErrors = append(authorityErrors, errors.Wrap(err, "fetching FulcioRoot")) - continue - } - var rekorClient *client.Rekor - if authority.CTLog != nil && authority.CTLog.URL != nil { - logging.FromContext(ctx).Debugf("Using CTLog %s for %s", authority.CTLog.URL, ref.Name()) - rekorClient, err = rekor.GetRekorClient(authority.CTLog.URL.String()) - if err != nil { - logging.FromContext(ctx).Errorf("failed creating rekor client: +v", err) - authorityErrors = append(authorityErrors, errors.Wrap(err, "creating Rekor client")) - continue - } - } - sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, remoteOpts...) - if err != nil { - logging.FromContext(ctx).Errorf("failed validSignatures with fulcio for %s: %v", ref.Name(), err) - authorityErrors = append(authorityErrors, errors.Wrap(err, "validate signatures with fulcio")) - } else { - if len(sps) > 0 { - logging.FromContext(ctx).Debugf("validated signature for %s, got %d signatures", ref.Name(), len(sps)) - return sps, nil - } - logging.FromContext(ctx).Errorf("no validSignatures found for %s", ref.Name()) - authorityErrors = append(authorityErrors, fmt.Errorf("no valid signatures found for %s", ref.Name())) - } + if err := policy.EvaluatePolicyAgainstJSON(ctx, wantedAttestation.Name, wantedAttestation.Type, wantedAttestation.Data, attBytes); err != nil { + return nil, err } + // Ok, so this passed aok, jot it down to our result set as + // verified attestation with the predicate type match + ret[wantedAttestation.Name] = ociSignatureToPolicySignature(ctx, verifiedAttestations) } } - return nil, authorityErrors + return ret, nil } // ResolvePodSpecable implements duckv1.PodSpecValidator diff --git a/pkg/cosign/kubernetes/webhook/validator_result.go b/pkg/cosign/kubernetes/webhook/validator_result.go new file mode 100644 index 00000000000..16a7b549c69 --- /dev/null +++ b/pkg/cosign/kubernetes/webhook/validator_result.go @@ -0,0 +1,60 @@ +// +// Copyright 2022 The Sigstore 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 webhook + +// PolicyResult is the result of a successful ValidatePolicy call. +// These are meant to be consumed by a higher level Policy engine that +// can reason about validated results. The 'first' level pass will verify +// signatures and attestations, and make the results then available for +// a policy that can be used to gate a passing of a ClusterImagePolicy. +// Some examples are, at least 'vulnerability' has to have been done +// and the scan must have been attested by a particular entity (sujbect/issuer) +// or a particular key. +// Other examples are N-of-M must be satisfied and so forth. +// We do not expose the low level details of signatures / attestations here +// since they have already been validated as per the Authority configuration +// and optionally by the Attestations which contain a particular policy that +// can be used to validate the Attestations (say vulnerability scanner must not +// have any High sev issues). +type PolicyResult struct { + // AuthorityMatches will have an entry for each successful Authority check + // on it. Key in the map is the Attestation.Name + AuthorityMatches map[string]AuthorityMatch `json:"authorityMatches"` +} + +// AuthorityMatch returns either Signatures (if there are no Attestations +// specified), or Attestations if there are Attestations specified. +type AuthorityMatch struct { + // All of the matching signatures for this authority + // Wonder if for consistency this should also have the matching + // attestations name, aka, make this into a map. + Signatures []PolicySignature `json:"signatures"` + + // Mapping from attestation name to all of verified attestations + Attestations map[string][]PolicySignature `json:"attestations"` +} + +// PolicySignature contains a normalized result of a validated signature, where +// signature could be a signature on the Image (.sig) or on an Attestation +// (.att). +type PolicySignature struct { + // Subject that was found to match on the Cert. + Subject string `json:"subject"` + // Issure that was found to match on the Cert. + Issuer string `json:"issuer"` + // TODO(vaikas): Add all the Fulcio specific extensions here too. + // https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md +} diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index ad9b3ac56d8..4dedd275ac5 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -18,12 +18,16 @@ package webhook import ( "bytes" "context" + "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/x509" "errors" + "fmt" "net/http" "net/http/httptest" + "reflect" + "strings" "testing" "time" @@ -52,6 +56,12 @@ import ( const ( fulcioRootCert = "-----BEGIN CERTIFICATE-----\nMIICNzCCAd2gAwIBAgITPLBoBQhl1hqFND9S+SGWbfzaRTAKBggqhkjOPQQDAjBo\nMQswCQYDVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlw\ncGVuaGFtMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMI\ndGVzdGNlcnQwHhcNMjEwMzEyMjMyNDQ5WhcNMzEwMjI4MjMyNDQ5WjBoMQswCQYD\nVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlwcGVuaGFt\nMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMIdGVzdGNl\ncnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQRn+Alyof6xP3GQClSwgV0NFuY\nYEwmKP/WLWr/LwB6LUYzt5v49RlqG83KuaJSpeOj7G7MVABdpIZYWwqAiZV3o2Yw\nZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQU\nT8Jwm6JuVb0dsiuHUROiHOOVHVkwHwYDVR0jBBgwFoAUT8Jwm6JuVb0dsiuHUROi\nHOOVHVkwCgYIKoZIzj0EAwIDSAAwRQIhAJkNZmP6sKA+8EebRXFkBa9DPjacBpTc\nOljJotvKidRhAiAuNrIazKEw2G4dw8x1z6EYk9G+7fJP5m93bjm/JfMBtA==\n-----END CERTIFICATE-----" rekorResponse = "bad response" + + // Random public key (cosign generate-key-pair) 2022-03-18 + authorityKeyCosignPubString = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENAyijLvRu5QpCPp2uOj8C79ZW1VJ +SID/4H61ZiRzN4nqONzp+ZF22qQTk3MFO3D0/ZKmWHAosIf2pf2GHH7myA== +-----END PUBLIC KEY-----` ) func TestValidatePodSpec(t *testing.T) { @@ -87,11 +97,6 @@ func TestValidatePodSpec(t *testing.T) { } var authorityKeyCosignPub *ecdsa.PublicKey - // Random public key (cosign generate-key-pair) 2022-03-18 - authorityKeyCosignPubString := `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENAyijLvRu5QpCPp2uOj8C79ZW1VJ -SID/4H61ZiRzN4nqONzp+ZF22qQTk3MFO3D0/ZKmWHAosIf2pf2GHH7myA== ------END PUBLIC KEY-----` pems := parsePems([]byte(authorityKeyCosignPubString)) if len(pems) > 0 { @@ -240,7 +245,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== { Key: &webhookcip.KeyRef{ Data: authorityKeyCosignPubString, - PublicKeys: []*ecdsa.PublicKey{authorityKeyCosignPub}, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, }, }, }, @@ -272,7 +277,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }}, Authorities: []webhookcip.Authority{ { - Keyless: &v1alpha1.KeylessRef{ + Keyless: &webhookcip.KeylessRef{ URL: badURL, }, }, @@ -285,10 +290,10 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = digest.String() + fe.Details = fmt.Sprintf("%s %s", digest.String(), `fetching FulcioRoot: getting root cert: parse "http://http:%2F%2Fexample.com%2F/api/v1/rootCert": invalid port ":%2F%2Fexample.com%2F" after host`) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = digest.String() + fe2.Details = fmt.Sprintf("%s %s", digest.String(), `fetching FulcioRoot: getting root cert: parse "http://http:%2F%2Fexample.com%2F/api/v1/rootCert": invalid port ":%2F%2Fexample.com%2F" after host`) errs = errs.Also(fe2) return errs }(), @@ -315,7 +320,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }}, Authorities: []webhookcip.Authority{ { - Keyless: &v1alpha1.KeylessRef{ + Keyless: &webhookcip.KeylessRef{ URL: fulcioURL, }, }, @@ -328,14 +333,95 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = digest.String() + fe.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = digest.String() + fe2.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe2) return errs }(), cvs: fail, + }, { + name: "simple, authority keyless checks out, good fulcio, bad cip policy", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Regex: ".*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + }, + }, + Policy: &webhookcip.AttestationPolicy{ + Name: "invalid json policy", + Type: "cue", + Data: `{"wontgo}`, + }, + }, + }, + }, + }, + ), + want: func() *apis.FieldError { + var errs *apis.FieldError + fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) + fe.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : failed to compile the cue policy with error: string literal not terminated", digest.String()) + errs = errs.Also(fe) + fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) + fe2.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : failed to compile the cue policy with error: string literal not terminated", digest.String()) + errs = errs.Also(fe2) + return errs + }(), + cvs: pass, + }, { + name: "simple, no error, authority keyless, good fulcio", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Regex: ".*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: pass, }, { name: "simple, error, authority keyless, good fulcio, bad rekor", ps: &corev1.PodSpec{ @@ -358,7 +444,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }}, Authorities: []webhookcip.Authority{ { - Keyless: &v1alpha1.KeylessRef{ + Keyless: &webhookcip.KeylessRef{ URL: fulcioURL, }, CTLog: &v1alpha1.TLog{ @@ -374,10 +460,10 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = digest.String() + fe.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = digest.String() + fe2.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe2) return errs }(), @@ -1103,3 +1189,262 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }) } } + +func TestValidatePolicy(t *testing.T) { + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + ctx, _ := rtesting.SetupFakeContext(t) + si := fakesecret.Get(ctx) + + secretName := "blah" + + // Non-existent URL for testing complete failure + badURL := apis.HTTP("http://example.com/") + t.Logf("badURL: %s", badURL.String()) + + // Spin up a Fulcio that responds with a Root Cert + fulcioServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte(fulcioRootCert)) + })) + t.Cleanup(fulcioServer.Close) + fulcioURL, err := apis.ParseURL(fulcioServer.URL) + if err != nil { + t.Fatalf("Failed to parse fake Fulcio URL") + } + t.Logf("fulcioURL: %s", fulcioURL.String()) + + rekorServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte(rekorResponse)) + })) + t.Cleanup(rekorServer.Close) + rekorURL, err := apis.ParseURL(rekorServer.URL) + if err != nil { + t.Fatalf("Failed to parse fake Rekor URL") + } + t.Logf("rekorURL: %s", rekorURL.String()) + var authorityKeyCosignPub *ecdsa.PublicKey + + pems := parsePems([]byte(authorityKeyCosignPubString)) + if len(pems) > 0 { + key, _ := x509.ParsePKIXPublicKey(pems[0].Bytes) + authorityKeyCosignPub = key.(*ecdsa.PublicKey) + } else { + t.Errorf("Error parsing authority key from string") + } + + si.Informer().GetIndexer().Add(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: system.Namespace(), + Name: secretName, + }, + Data: map[string][]byte{ + // Random public key (cosign generate-key-pair) 2021-09-25 + "cosign.pub": []byte(`-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEapTW568kniCbL0OXBFIhuhOboeox +UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== +-----END PUBLIC KEY----- +`), + }, + }) + + cvs := cosignVerifySignatures + defer func() { + cosignVerifySignatures = cvs + }() + // Let's just say that everything is verified. + pass := func(_ context.Context, _ name.Reference, _ *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + sig, err := static.NewSignature(nil, "") + if err != nil { + return nil, false, err + } + return []oci.Signature{sig}, true, nil + } + // Let's just say that everything is not verified. + fail := func(_ context.Context, _ name.Reference, _ *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + return nil, false, errors.New("bad signature") + } + + // Let's say it is verified if it is the expected Public Key + authorityPublicKeyCVS := func(ctx context.Context, signedImgRef name.Reference, co *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + actualPublicKey, _ := co.SigVerifier.PublicKey() + actualECDSAPubkey := actualPublicKey.(*ecdsa.PublicKey) + actualKeyData := elliptic.Marshal(actualECDSAPubkey, actualECDSAPubkey.X, actualECDSAPubkey.Y) + + expectedKeyData := elliptic.Marshal(authorityKeyCosignPub, authorityKeyCosignPub.X, authorityKeyCosignPub.Y) + + if bytes.Equal(actualKeyData, expectedKeyData) { + return pass(ctx, signedImgRef, co) + } + + return fail(ctx, signedImgRef, co) + } + + tests := []struct { + name string + policy webhookcip.ClusterImagePolicy + want *PolicyResult + wantErrs []string + cva func(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error) + cvs func(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error) + customContext context.Context + }{{ + name: "simple, public key, no matches", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + }, + wantErrs: []string{"failed to validate public keys with authority authority-0 for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"}, + cvs: fail, + }, { + name: "simple, public key, works", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + }, + want: &PolicyResult{ + AuthorityMatches: map[string]AuthorityMatch{ + "authority-0": { + Signatures: []PolicySignature{{ + Subject: "PLACEHOLDER", + Issuer: "PLACEHOLDER"}}, + }}, + }, + cvs: pass, + }, { + name: "simple, public key, no error", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + }, + want: &PolicyResult{ + AuthorityMatches: map[string]AuthorityMatch{ + "authority-0": { + Signatures: []PolicySignature{{ + Subject: "PLACEHOLDER", + Issuer: "PLACEHOLDER"}}, + }}, + }, + cvs: authorityPublicKeyCVS, + }, { + name: "simple, keyless attestation, works", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + Attestations: []webhookcip.AttestationPolicy{{ + Name: "test-att", + PredicateType: "custom", + }}, + }, + }, + }, + want: &PolicyResult{ + AuthorityMatches: map[string]AuthorityMatch{ + "authority-0": { + Attestations: map[string][]PolicySignature{"test-att": {{ + Subject: "PLACEHOLDER", + Issuer: "PLACEHOLDER"}}, + }}}, + }, + cva: pass, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cosignVerifySignatures = test.cvs + cosignVerifyAttestations = test.cva + testContext := context.Background() + + if test.customContext != nil { + testContext = test.customContext + } + got, gotErrs := ValidatePolicy(testContext, digest, test.policy) + validateErrors(t, test.wantErrs, gotErrs) + if !reflect.DeepEqual(test.want, got) { + t.Errorf("unexpected PolicyResult, want: %+v got: %+v", test.want, got) + } + }) + } +} + +func validateErrors(t *testing.T, wantErr []string, got []error) { + t.Helper() + if len(wantErr) != len(got) { + t.Errorf("Wanted %d errors got %d", len(wantErr), len(got)) + } else { + for i, want := range wantErr { + if !strings.Contains(got[i].Error(), want) { + t.Errorf("Unwanted error at %d want: %s got: %s", i, want, got[i]) + } + } + } +} + +func TestValidatePolicyCancelled(t *testing.T) { + var authorityKeyCosignPub *ecdsa.PublicKey + pems := parsePems([]byte(authorityKeyCosignPubString)) + if len(pems) > 0 { + key, _ := x509.ParsePKIXPublicKey(pems[0].Bytes) + authorityKeyCosignPub = key.(*ecdsa.PublicKey) + } else { + t.Errorf("Error parsing authority key from string") + } + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + testContext, cancelFunc := context.WithCancel(context.Background()) + cip := webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + } + wantErrs := []string{"context was canceled before validation completed"} + cancelFunc() + _, gotErrs := ValidatePolicy(testContext, digest, cip) + validateErrors(t, wantErrs, gotErrs) +} + +func TestValidatePoliciesCancelled(t *testing.T) { + var authorityKeyCosignPub *ecdsa.PublicKey + pems := parsePems([]byte(authorityKeyCosignPubString)) + if len(pems) > 0 { + key, _ := x509.ParsePKIXPublicKey(pems[0].Bytes) + authorityKeyCosignPub = key.(*ecdsa.PublicKey) + } else { + t.Errorf("Error parsing authority key from string") + } + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + testContext, cancelFunc := context.WithCancel(context.Background()) + cip := webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + } + wantErrs := []string{"context was canceled before validation completed"} + cancelFunc() + _, gotErrs := validatePolicies(testContext, digest, map[string]webhookcip.ClusterImagePolicy{"testcip": cip}) + validateErrors(t, wantErrs, gotErrs["internalerror"]) +} diff --git a/pkg/cosign/rego/rego.go b/pkg/cosign/rego/rego.go index f3def4dcc76..2f4a6e0513a 100644 --- a/pkg/cosign/rego/rego.go +++ b/pkg/cosign/rego/rego.go @@ -22,6 +22,7 @@ import ( "fmt" "github.com/open-policy-agent/opa/rego" + "knative.dev/pkg/logging" ) // The query below should meet the following requirements: @@ -29,6 +30,12 @@ import ( // * Queries for a single value. const QUERY = "data.signature.allow" +// CosignRegoPackageName defines the expected package name of a provided rego module +const CosignRegoPackageName = "sigstore" + +// CosignEvaluationRule defines the expected evaluation role of a provided rego module +const CosignEvaluationRule = "isCompliant" + func ValidateJSON(jsonBody []byte, entrypoints []string) []error { ctx := context.Background() @@ -73,3 +80,42 @@ func ValidateJSON(jsonBody []byte, entrypoints []string) []error { } return errs } + +// ValidateJSONWithModuleInput takes the body of the results to evaluate and the defined module +// in a policy to validate against the input data +func ValidateJSONWithModuleInput(jsonBody []byte, moduleInput string) error { + ctx := context.Background() + query := fmt.Sprintf("%s = data.%s.%s", CosignEvaluationRule, CosignRegoPackageName, CosignEvaluationRule) + module := fmt.Sprintf("%s.rego", CosignRegoPackageName) + + r := rego.New( + rego.Query(query), + rego.Module(module, moduleInput)) + + evalQuery, err := r.PrepareForEval(ctx) + if err != nil { + return err + } + + var input interface{} + dec := json.NewDecoder(bytes.NewBuffer(jsonBody)) + dec.UseNumber() + if err := dec.Decode(&input); err != nil { + return err + } + + rs, err := evalQuery.Eval(ctx, rego.EvalInput(input)) + if err != nil { + return err + } + + for _, result := range rs { + isCompliant, ok := result.Bindings[CosignEvaluationRule].(bool) + if ok && isCompliant { + logging.FromContext(ctx).Info("Validated policy is compliant") + return nil + } + } + + return fmt.Errorf("policy is not compliant for query '%s'", query) +} diff --git a/pkg/cosign/rego/rego_test.go b/pkg/cosign/rego/rego_test.go index 875249594a1..a1fb4e3541a 100644 --- a/pkg/cosign/rego/rego_test.go +++ b/pkg/cosign/rego/rego_test.go @@ -98,3 +98,118 @@ func TestValidationJSON(t *testing.T) { }) } } + +const attestationsJSONBody = `{ + "authorityMatches": { + "keyatt": { + "signatures": null, + "attestations": { + "vuln-key": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keysignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + }, + "keylessatt": { + "signatures": null, + "attestations": { + "custom-keyless": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keylesssignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + } + } + }` + +func TestValidateJSONWithModuleInput(t *testing.T) { + cases := []struct { + name string + jsonBody string + policy string + pass bool + errorMsg string + }{ + { + name: "passing policy attestations", + jsonBody: attestationsJSONBody, + policy: ` + package sigstore + default isCompliant = false + isCompliant { + attestationsKeylessATT := input.authorityMatches.keylessatt.attestations + count(attestationsKeylessATT) == 1 + + attestationsKeyATT := input.authorityMatches.keyatt.attestations + count(attestationsKeyATT) == 1 + + keylessSignature := input.authorityMatches.keylesssignature.signatures + count(keylessSignature) == 1 + + keySignature := input.authorityMatches.keysignature.signatures + count(keySignature) == 1 + } + `, + pass: true, + }, + { + name: "not passing policy attestations", + jsonBody: attestationsJSONBody, + policy: ` + package sigstore + + default isCompliant = false + + isCompliant { + attestationsKeylessATT := input.authorityMatches.keylessatt.attestations + count(attestationsKeylessATT) == 0 + + attestationsKeyATT := input.authorityMatches.keyatt.attestations + count(attestationsKeyATT) == 1 + + keylessSignature := input.authorityMatches.keylesssignature.signatures + count(keylessSignature) == 1 + + keySignature := input.authorityMatches.keysignature.signatures + count(keySignature) == 1 + } + `, + pass: false, + errorMsg: "policy is not compliant for query 'isCompliant = data.sigstore.isCompliant'", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateJSONWithModuleInput([]byte(tt.jsonBody), tt.policy); (err == nil) != tt.pass { + t.Fatalf("Unexpected result: %v", err) + } else if err != nil { + if fmt.Sprintf("%s", err) != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err) + } + } + }) + } +} diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index ed0e6e31356..21970b280e8 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -17,8 +17,10 @@ package cosign import ( "bytes" "context" + "crypto" "crypto/ecdsa" "crypto/sha256" + "crypto/x509" "encoding/base64" "encoding/hex" "fmt" @@ -63,9 +65,19 @@ const ( addRekorPublicKeyFromRekor = "SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY" ) +// getLogID generates a SHA256 hash of a DER-encoded public key. +func getLogID(pub crypto.PublicKey) (string, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return "", err + } + digest := sha256.Sum256(pubBytes) + return hex.EncodeToString(digest[:]), nil +} + // GetRekorPubs retrieves trusted Rekor public keys from the embedded or cached // TUF root. If expired, makes a network call to retrieve the updated targets. -func GetRekorPubs(ctx context.Context) ([]RekorPubKey, error) { +func GetRekorPubs(ctx context.Context) (map[string]RekorPubKey, error) { tufClient, err := tuf.NewFromEnv(ctx) if err != nil { return nil, err @@ -75,7 +87,7 @@ func GetRekorPubs(ctx context.Context) ([]RekorPubKey, error) { if err != nil { return nil, err } - publicKeys := make([]RekorPubKey, 0, len(targets)) + publicKeys := make(map[string]RekorPubKey) altRekorPub := os.Getenv(altRekorPublicKey) if altRekorPub != "" { fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for Rekor: %s\n", altRekorPub) @@ -87,14 +99,22 @@ func GetRekorPubs(ctx context.Context) ([]RekorPubKey, error) { if err != nil { return nil, errors.Wrap(err, "error converting PEM to ECDSAKey") } - publicKeys = append(publicKeys, RekorPubKey{PubKey: extra, Status: tuf.Active}) + keyID, err := getLogID(extra) + if err != nil { + return nil, errors.Wrap(err, "error generating log ID") + } + publicKeys[keyID] = RekorPubKey{PubKey: extra, Status: tuf.Active} } else { for _, t := range targets { rekorPubKey, err := PemToECDSAKey(t.Target) if err != nil { return nil, errors.Wrap(err, "pem to ecdsa") } - publicKeys = append(publicKeys, RekorPubKey{PubKey: rekorPubKey, Status: t.Status}) + keyID, err := getLogID(rekorPubKey) + if err != nil { + return nil, errors.Wrap(err, "error generating log ID") + } + publicKeys[keyID] = RekorPubKey{PubKey: rekorPubKey, Status: t.Status} } } if len(publicKeys) == 0 { @@ -353,19 +373,23 @@ func VerifyTLogEntry(ctx context.Context, rekorClient *client.Rekor, e *models.L if err != nil { return errors.Wrap(err, "error converting rekor PEM public key from rekor to ECDSAKey") } - rekorPubKeys = append(rekorPubKeys, RekorPubKey{PubKey: pubFromAPI, Status: tuf.Active}) + keyID, err := getLogID(pubFromAPI) + if err != nil { + return errors.Wrap(err, "error generating log ID") + } + rekorPubKeys[keyID] = RekorPubKey{PubKey: pubFromAPI, Status: tuf.Active} } - var entryVerError error - for _, pubKey := range rekorPubKeys { - entryVerError = VerifySET(payload, []byte(e.Verification.SignedEntryTimestamp), pubKey.PubKey) - // Return once the SET is verified successfully. - if entryVerError == nil { - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - return nil - } + pubKey, ok := rekorPubKeys[payload.LogID] + if !ok { + return errors.New("rekor log public key not found for payload") } - return errors.Wrap(entryVerError, "verifying signedEntryTimestamp") + err = VerifySET(payload, []byte(e.Verification.SignedEntryTimestamp), pubKey.PubKey) + if err != nil { + return errors.Wrap(err, "verifying signedEntryTimestamp") + } + if pubKey.Status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") + } + return nil } diff --git a/pkg/cosign/tlog_test.go b/pkg/cosign/tlog_test.go index 9b773271f67..03b54709796 100644 --- a/pkg/cosign/tlog_test.go +++ b/pkg/cosign/tlog_test.go @@ -27,4 +27,14 @@ func TestGetRekorPubKeys(t *testing.T) { if len(keys) == 0 { t.Errorf("expected 1 or more keys, got 0") } + // check that the mapping of key digest to key is correct + for logID, key := range keys { + expectedLogID, err := getLogID(key.PubKey) + if err != nil { + t.Fatalf("unexpected error generated log ID: %v", err) + } + if logID != expectedLogID { + t.Fatalf("key digests are not equal") + } + } } diff --git a/pkg/cosign/tuf/client.go b/pkg/cosign/tuf/client.go index ab632fac130..b37902bc3bc 100644 --- a/pkg/cosign/tuf/client.go +++ b/pkg/cosign/tuf/client.go @@ -22,6 +22,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/url" "os" "path" @@ -55,10 +56,17 @@ type TUF struct { // JSON output representing the configured root status type RootStatus struct { - Local string `json:"local"` - Remote string `json:"remote"` - Expiration map[string]string `json:"expiration"` - Targets []string `json:"targets"` + Local string `json:"local"` + Remote string `json:"remote"` + Metadata map[string]MetadataStatus `json:"metadata"` + Targets []string `json:"targets"` +} + +type MetadataStatus struct { + Version int `json:"version"` + Size int `json:"len"` + Expiration string `json:"expiration"` + Error string `json:"error"` } type TargetFile struct { @@ -75,20 +83,64 @@ type sigstoreCustomMetadata struct { Sigstore customMetadata `json:"sigstore"` } +type signedMeta struct { + Type string `json:"_type"` + Expires time.Time `json:"expires"` + Version int64 `json:"version"` +} + // RemoteCache contains information to cache on the location of the remote // repository. type remoteCache struct { Mirror string `json:"mirror"` } -// GetRootStatus gets the current root status for info logging -func GetRootStatus(ctx context.Context) (*RootStatus, error) { - t, err := NewFromEnv(ctx) +func getExpiration(metadata []byte) (*time.Time, error) { + s := &data.Signed{} + if err := json.Unmarshal(metadata, s); err != nil { + return nil, err + } + sm := &signedMeta{} + if err := json.Unmarshal(s.Signed, sm); err != nil { + return nil, err + } + return &sm.Expires, nil +} + +func getVersion(metadata []byte) (int64, error) { + s := &data.Signed{} + if err := json.Unmarshal(metadata, s); err != nil { + return 0, err + } + sm := &signedMeta{} + if err := json.Unmarshal(s.Signed, sm); err != nil { + return 0, err + } + return sm.Version, nil +} + +var isExpiredTimestamp = func(metadata []byte) bool { + expiration, err := getExpiration(metadata) + if err != nil { + return true + } + return time.Until(*expiration) <= 0 +} + +func getMetadataStatus(b []byte) (*MetadataStatus, error) { + expires, err := getExpiration(b) if err != nil { return nil, err } - defer t.Close() - return t.getRootStatus() + version, err := getVersion(b) + if err != nil { + return nil, err + } + return &MetadataStatus{ + Size: len(b), + Expiration: expires.Format(time.RFC822), + Version: int(version), + }, nil } func (t *TUF) getRootStatus() (*RootStatus, error) { @@ -97,10 +149,10 @@ func (t *TUF) getRootStatus() (*RootStatus, error) { local = rootCacheDir() } status := &RootStatus{ - Local: local, - Remote: t.mirror, - Expiration: map[string]string{}, - Targets: []string{}, + Local: local, + Remote: t.mirror, + Metadata: make(map[string]MetadataStatus), + Targets: []string{}, } // Get targets @@ -118,16 +170,40 @@ func (t *TUF) getRootStatus() (*RootStatus, error) { return nil, errors.Wrap(err, "getting trusted meta") } for role, md := range trustedMeta { - expires, err := getExpiration(md) + mdStatus, err := getMetadataStatus(md) if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("getting expiration for %s", role)) + status.Metadata[role] = MetadataStatus{Error: err.Error()} + continue } - status.Expiration[role] = expires.Format(time.RFC822) + status.Metadata[role] = *mdStatus } return status, nil } +func getRoot(meta map[string]json.RawMessage) (json.RawMessage, error) { + trustedRoot, ok := meta["root.json"] + if ok { + return trustedRoot, nil + } + // On first initialize, there will be no root in the TUF DB, so read from embedded. + trustedRoot, err := embeddedRootRepo.ReadFile(path.Join("repository", "root.json")) + if err != nil { + return nil, err + } + return trustedRoot, nil +} + +// GetRootStatus gets the current root status for info logging +func GetRootStatus(ctx context.Context) (*RootStatus, error) { + t, err := NewFromEnv(ctx) + if err != nil { + return nil, err + } + defer t.Close() + return t.getRootStatus() +} + // Close closes the local TUF store. Should only be called once per client. func (t *TUF) Close() error { return t.local.Close() @@ -239,19 +315,6 @@ func NewFromEnv(ctx context.Context) (*TUF, error) { return initializeTUF(ctx, embed, mirror, nil, false) } -func getRoot(meta map[string]json.RawMessage) (json.RawMessage, error) { - trustedRoot, ok := meta["root.json"] - if ok { - return trustedRoot, nil - } - // On first initialize, there will be no root in the TUF DB, so read from embedded. - trustedRoot, err := embeddedRootRepo.ReadFile(path.Join("repository", "root.json")) - if err != nil { - return nil, err - } - return trustedRoot, nil -} - func Initialize(ctx context.Context, mirror string, root []byte) error { // Initialize the client. Force an update. t, err := initializeTUF(ctx, false, mirror, root, true) @@ -360,34 +423,41 @@ func embeddedLocalStore() (client.LocalStore, error) { return local, nil } -//go:embed repository -var embeddedRootRepo embed.FS - -func getExpiration(metadata []byte) (*time.Time, error) { - s := &data.Signed{} - if err := json.Unmarshal(metadata, s); err != nil { - return nil, err - } - sm := &data.Timestamp{} - if err := json.Unmarshal(s.Signed, sm); err != nil { - return nil, err - } - return &sm.Expires, nil -} - -var isExpiredTimestamp = func(metadata []byte) bool { - expiration, err := getExpiration(metadata) - if err != nil { - return true - } - return time.Until(*expiration) <= 0 -} - func (t *TUF) updateMetadataAndDownloadTargets() error { // Download updated targets and cache new metadata and targets in ${TUF_ROOT}. targetFiles, err := t.client.Update() if err != nil && !client.IsLatestSnapshot(err) { - return errors.Wrap(err, "updating tuf metadata") + // Get some extra information for debugging. What was the state of the metadata + // on the remote? + status := struct { + Mirror string `json:"mirror"` + Metadata map[string]MetadataStatus `json:"metadata"` + }{ + Mirror: t.mirror, + Metadata: make(map[string]MetadataStatus), + } + for _, md := range []string{"root.json", "targets.json", "snapshot.json", "timestamp.json"} { + r, _, err := t.remote.GetMeta(md) + if err != nil { + // May be missing, or failed download. + continue + } + defer r.Close() + b, err := ioutil.ReadAll(r) + if err != nil { + continue + } + mdStatus, err := getMetadataStatus(b) + if err != nil { + continue + } + status.Metadata[md] = *mdStatus + } + b, innerErr := json.MarshalIndent(status, "", "\t") + if innerErr != nil { + return innerErr + } + return fmt.Errorf("error updating to TUF remote mirror: %w\nremote status:%s", err, string(b)) } // Update the in-memory targets. @@ -405,15 +475,6 @@ func (t *TUF) updateMetadataAndDownloadTargets() error { return nil } -func downloadRemoteTarget(name string, c *client.Client, w io.Writer) error { - dest := targetDestination{} - if err := c.Download(name, &dest); err != nil { - return errors.Wrap(err, "downloading target") - } - _, err := io.Copy(w, &dest.buf) - return err -} - type targetDestination struct { buf bytes.Buffer } @@ -427,6 +488,15 @@ func (t *targetDestination) Delete() error { return nil } +func downloadRemoteTarget(name string, c *client.Client, w io.Writer) error { + dest := targetDestination{} + if err := c.Download(name, &dest); err != nil { + return errors.Wrap(err, "downloading target") + } + _, err := io.Copy(w, &dest.buf) + return err +} + func rootCacheDir() string { rootDir := os.Getenv(TufRootEnv) if rootDir == "" { @@ -468,6 +538,9 @@ func (m *memoryCache) Set(p string, b []byte) error { return nil } +//go:embed repository +var embeddedRootRepo embed.FS + type embedded struct { setImpl } diff --git a/pkg/cosign/tuf/testutils.go b/pkg/cosign/tuf/testutils.go new file mode 100644 index 00000000000..729af872db6 --- /dev/null +++ b/pkg/cosign/tuf/testutils.go @@ -0,0 +1,126 @@ +// +// Copyright 2021 The Sigstore 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 tuf + +import ( + "context" + "crypto/x509" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" + "github.com/theupdateframework/go-tuf" +) + +type TestSigstoreRoot struct { + Rekor signature.Verifier + FulcioCertificate *x509.Certificate + // TODO: Include a CTFE key if/when cosign verifies SCT. +} + +// This creates a new sigstore TUF repo whose signers can be used to create dynamic +// signed Rekor entries. +func NewSigstoreTufRepo(t *testing.T, root TestSigstoreRoot) (tuf.LocalStore, *tuf.Repo) { + td := t.TempDir() + ctx := context.Background() + remote := tuf.FileSystemStore(td, nil) + r, err := tuf.NewRepo(remote) + if err != nil { + t.Error(err) + } + if err := r.Init(false); err != nil { + t.Error(err) + } + + for _, role := range []string{"root", "targets", "snapshot", "timestamp"} { + if _, err := r.GenKey(role); err != nil { + t.Error(err) + } + } + targetsPath := filepath.Join(td, "staged", "targets") + if err := os.MkdirAll(filepath.Dir(targetsPath), 0755); err != nil { + t.Error(err) + } + // Add the rekor key target + pk, err := root.Rekor.PublicKey(options.WithContext(ctx)) + if err != nil { + t.Error(err) + } + b, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + t.Error(err) + } + rekorPath := "rekor.pub" + rekorData := cryptoutils.PEMEncode(cryptoutils.PublicKeyPEMType, b) + if err := ioutil.WriteFile(filepath.Join(targetsPath, rekorPath), rekorData, 0600); err != nil { + t.Error(err) + } + scmRekor, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Rekor, Status: Active}}) + if err != nil { + t.Error(err) + } + if err := r.AddTarget("rekor.pub", scmRekor); err != nil { + t.Error(err) + } + // Add Fulcio Certificate information. + fulcioPath := "fulcio.crt.pem" + fulcioData := cryptoutils.PEMEncode(cryptoutils.CertificatePEMType, root.FulcioCertificate.Raw) + if err := ioutil.WriteFile(filepath.Join(targetsPath, fulcioPath), fulcioData, 0600); err != nil { + t.Error(err) + } + scmFulcio, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Fulcio, Status: Active}}) + if err != nil { + t.Error(err) + } + if err := r.AddTarget(fulcioPath, scmFulcio); err != nil { + t.Error(err) + } + if err := r.Snapshot(); err != nil { + t.Error(err) + } + if err := r.Timestamp(); err != nil { + t.Error(err) + } + if err := r.Commit(); err != nil { + t.Error(err) + } + // Serve remote repository. + s := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(td, "repository")))) + defer s.Close() + + // Initialize with custom root. + tufRoot := t.TempDir() + t.Setenv("TUF_ROOT", tufRoot) + meta, err := remote.GetMeta() + if err != nil { + t.Error(err) + } + rootBytes, ok := meta["root.json"] + if !ok { + t.Error(err) + } + if err := Initialize(ctx, s.URL, rootBytes); err != nil { + t.Error(err) + } + return remote, r +} diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 10d7aedd119..e2e9d98a368 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -27,9 +27,11 @@ import ( "encoding/json" "fmt" "os" + "regexp" "strings" "time" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/pkg/cosign/tuf" @@ -55,6 +57,13 @@ import ( sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" ) +// Identity specifies an issuer/subject to verify a signature against. +// Both Issuer/Subject support regexp. +type Identity struct { + Issuer string + Subject string +} + // CheckOpts are the options for checking signatures. type CheckOpts struct { // RegistryClientOpts are the options for interacting with the container registry. @@ -81,9 +90,17 @@ type CheckOpts struct { CertEmail string // CertOidcIssuer is the OIDC issuer expected for a certificate to be valid. The empty string means any certificate can be valid. CertOidcIssuer string + // EnforceSCT requires that a certificate contain an embedded SCT during verification. An SCT is proof of inclusion in a + // certificate transparency log. + EnforceSCT bool // SignatureRef is the reference to the signature file SignatureRef string + + // Identities is an array of Identity (Subject, Issuer) matchers that have + // to be met for the signature to ve valid. + // Supercedes CertEmail / CertOidcIssuer + Identities []Identity } func getSignedEntity(signedImgRef name.Reference, regClientOpts []ociremote.Option) (oci.SignedEntity, v1.Hash, error) { @@ -143,7 +160,7 @@ func verifyOCIAttestation(_ context.Context, verifier signature.Verifier, att pa } // ValidateAndUnpackCert creates a Verifier from a certificate. Veries that the certificate -// chains up to a trusted root. Optionally verifies the subject of the certificate. +// chains up to a trusted root. Optionally verifies the subject and issuer of the certificate. func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) { verifier, err := signature.LoadVerifier(cert.PublicKey, crypto.SHA256) if err != nil { @@ -151,7 +168,8 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } // Now verify the cert, then the signature. - if err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts); err != nil { + chains, err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts) + if err != nil { return nil, err } if co.CertEmail != "" { @@ -167,23 +185,102 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } } if co.CertOidcIssuer != "" { - issuer := "" - for _, ext := range cert.Extensions { - if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}) { - issuer = string(ext.Value) - break + if getIssuer(cert) != co.CertOidcIssuer { + return nil, errors.New("expected oidc issuer not found in certificate") + } + } + // If there are identities given, go through them and if one of them + // matches, call that good, otherwise, return an error. + if len(co.Identities) > 0 { + for _, identity := range co.Identities { + issuerMatches := false + // Check the issuer first + if identity.Issuer != "" { + issuer := getIssuer(cert) + if regex, err := regexp.Compile(identity.Issuer); err != nil { + return nil, fmt.Errorf("malformed issuer in identity: %s : %w", identity.Issuer, err) + } else if regex.MatchString(issuer) { + issuerMatches = true + } + } else { + // No issuer constraint on this identity, so checks out + issuerMatches = true + } + + // Then the subject + subjectMatches := false + if identity.Subject != "" { + regex, err := regexp.Compile(identity.Subject) + if err != nil { + return nil, fmt.Errorf("malformed subject in identity: %s : %w", identity.Subject, err) + } + for _, san := range getSubjectAlternateNames(cert) { + if regex.MatchString(san) { + subjectMatches = true + break + } + } + } else { + // No subject constraint on this identity, so checks out + subjectMatches = true + } + if subjectMatches && issuerMatches { + // If both issuer / subject match, return verifier + return verifier, nil } } - if issuer != co.CertOidcIssuer { - return nil, errors.New("expected oidc issuer not found in certificate") + return nil, errors.New("none of the expected identities matched what was in the certificate") + } + contains, err := ctl.ContainsSCT(cert.Raw) + if err != nil { + return nil, err + } + if co.EnforceSCT && !contains { + return nil, errors.New("certificate does not include required embedded SCT") + } + if contains { + // handle if chains has more than one chain - grab first and print message + if len(chains) > 1 { + fmt.Fprintf(os.Stderr, "**Info** Multiple valid certificate chains found. Selecting the first to verify the SCT.\n") + } + if err := ctl.VerifyEmbeddedSCT(context.Background(), chains[0]); err != nil { + return nil, err } } return verifier, nil } -// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Veries that the certificate +// getSubjectAlternateNames returns all of the following for a Certificate. +// DNSNames +// EmailAddresses +// IPAddresses +// URIs +func getSubjectAlternateNames(cert *x509.Certificate) []string { + sans := []string{} + sans = append(sans, cert.DNSNames...) + sans = append(sans, cert.EmailAddresses...) + for _, ip := range cert.IPAddresses { + sans = append(sans, ip.String()) + } + for _, uri := range cert.URIs { + sans = append(sans, uri.String()) + } + return sans +} + +// getIssuer returns the issuer for a Certificate +func getIssuer(cert *x509.Certificate) string { + for _, ext := range cert.Extensions { + if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}) { + return string(ext.Value) + } + } + return "" +} + +// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Verifies that the certificate // chains up to the provided root. Chain should start with the parent of the certificate and end with the root. -// Optionally verifies the subject of the certificate. +// Optionally verifies the subject and issuer of the certificate. func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certificate, co *CheckOpts) (signature.Verifier, error) { if len(chain) == 0 { return nil, errors.New("no chain provided to validate certificate") @@ -378,7 +475,8 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co // If the chain annotation is not present or there is only a root if chain == nil || len(chain) <= 1 { co.IntermediateCerts = nil - } else { + } else if co.IntermediateCerts == nil { + // If the intermediate certs have not been loaded in by TUF pool := x509.NewCertPool() for _, cert := range chain[:len(chain)-1] { pool.AddCert(cert) @@ -556,7 +654,8 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash // If the chain annotation is not present or there is only a root if chain == nil || len(chain) <= 1 { co.IntermediateCerts = nil - } else { + } else if co.IntermediateCerts == nil { + // If the intermediate certs have not been loaded in by TUF pool := x509.NewCertPool() for _, cert := range chain[:len(chain)-1] { pool.AddCert(cert) @@ -645,31 +744,29 @@ func VerifyBundle(ctx context.Context, sig oci.Signature) (bool, error) { return false, errors.Wrap(err, "retrieving rekor public key") } - var entryVerError error - for _, pubKey := range publicKeys { - entryVerError = VerifySET(bundle.Payload, bundle.SignedEntryTimestamp, pubKey.PubKey) - // Exit early with successful verification - if entryVerError == nil { - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - break - } + pubKey, ok := publicKeys[bundle.Payload.LogID] + if !ok { + return false, errors.New("rekor log public key not found for payload") + } + err = VerifySET(bundle.Payload, bundle.SignedEntryTimestamp, pubKey.PubKey) + if err != nil { + return false, err } - if entryVerError != nil { - return false, entryVerError + if pubKey.Status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") } cert, err := sig.Cert() if err != nil { return false, err - } else if cert == nil { - return true, nil } - // verify the cert against the integrated time - if err := CheckExpiry(cert, time.Unix(bundle.Payload.IntegratedTime, 0)); err != nil { - return false, errors.Wrap(err, "checking expiry on cert") + if cert != nil { + // Verify the cert against the integrated time. + // Note that if the caller requires the certificate to be present, it has to ensure that itself. + if err := CheckExpiry(cert, time.Unix(bundle.Payload.IntegratedTime, 0)); err != nil { + return false, errors.Wrap(err, "checking expiry on cert") + } } payload, err := sig.Payload() @@ -833,8 +930,8 @@ func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa. return nil } -func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) error { - if _, err := cert.Verify(x509.VerifyOptions{ +func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) ([][]*x509.Certificate, error) { + chains, err := cert.Verify(x509.VerifyOptions{ // THIS IS IMPORTANT: WE DO NOT CHECK TIMES HERE // THE CERTIFICATE IS TREATED AS TRUSTED FOREVER // WE CHECK THAT THE SIGNATURES WERE CREATED DURING THIS WINDOW @@ -844,10 +941,11 @@ func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x5 KeyUsages: []x509.ExtKeyUsage{ x509.ExtKeyUsageCodeSigning, }, - }); err != nil { - return err + }) + if err != nil { + return nil, err } - return nil + return chains, nil } func correctAnnotations(wanted, have map[string]interface{}) bool { diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 0ff80e7def6..2c38ccd0cc4 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -15,8 +15,10 @@ package cosign import ( + "bytes" "context" "crypto" + "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" @@ -24,17 +26,29 @@ import ( "encoding/json" "encoding/pem" "io" + "net" + "net/url" + "os" "strings" "testing" + "time" + "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" + "github.com/go-openapi/strfmt" + "github.com/google/certificate-transparency-go/testdata" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/pkg/cosign/bundle" + ctuf "github.com/sigstore/cosign/pkg/cosign/tuf" "github.com/sigstore/cosign/pkg/oci/static" "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/cosign/test" + rtypes "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" "github.com/stretchr/testify/require" ) @@ -167,8 +181,54 @@ func TestVerifyImageSignatureMultipleSubs(t *testing.T) { } } +func signEntry(ctx context.Context, t *testing.T, signer signature.Signer, entry bundle.RekorPayload) []byte { + payload, err := json.Marshal(entry) + if err != nil { + t.Fatalf("marshalling error: %v", err) + } + canonicalized, err := jsoncanonicalizer.Transform(payload) + if err != nil { + t.Fatalf("canonicalizing error: %v", err) + } + signature, err := signer.SignMessage(bytes.NewReader(canonicalized), options.WithContext(ctx)) + if err != nil { + t.Fatalf("signing error: %v", err) + } + return signature +} + +func CreateTestBundle(ctx context.Context, t *testing.T, rekor signature.Signer, leaf []byte) *bundle.RekorBundle { + // generate log ID according to rekor public key + pk, _ := rekor.PublicKey(nil) + keyID, _ := getLogID(pk) + pyld := bundle.RekorPayload{ + Body: base64.StdEncoding.EncodeToString(leaf), + IntegratedTime: time.Now().Unix(), + LogIndex: 693591, + LogID: keyID, + } + // Sign with root. + signature := signEntry(ctx, t, rekor, pyld) + b := &bundle.RekorBundle{ + SignedEntryTimestamp: strfmt.Base64(signature), + Payload: pyld, + } + return b +} + func TestVerifyImageSignatureWithNoChain(t *testing.T) { + ctx := context.Background() rootCert, rootKey, _ := test.GenerateRootCa() + sv, _, err := signature.NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256) + if err != nil { + t.Fatalf("creating signer: %v", err) + } + testSigstoreRoot := ctuf.TestSigstoreRoot{ + Rekor: sv, + FulcioCertificate: rootCert, + } + _, _ = ctuf.NewSigstoreTufRepo(t, testSigstoreRoot) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) @@ -179,14 +239,21 @@ func TestVerifyImageSignatureWithNoChain(t *testing.T) { h := sha256.Sum256(payload) signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, []byte{})) + // Create a fake bundle + pe, _ := proposedEntry(base64.StdEncoding.EncodeToString(signature), payload, pemLeaf) + entry, _ := rtypes.NewEntry(pe[0]) + leaf, _ := entry.Canonicalize(ctx) + rekorBundle := CreateTestBundle(ctx, t, sv, leaf) + + opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} + ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool}) if err != nil { t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) } - // TODO: Create fake bundle and test verification - if verified == true { - t.Fatalf("expected verified=false, got verified=true") + if verified == false { + t.Fatalf("expected verified=true, got verified=false") } } @@ -242,6 +309,42 @@ func TestVerifyImageSignatureWithMissingSub(t *testing.T) { } } +func TestVerifyImageSignatureWithExistingSub(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + otherSubCert, _, _ := test.GenerateSubordinateCa(rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + subPool := x509.NewCertPool() + // Load in different sub cert so the chain doesn't verify + rootPool.AddCert(otherSubCert) + + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + ociSig, _ := static.NewSignature(payload, + base64.StdEncoding.EncodeToString(signature), + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot}))) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool, IntermediateCerts: subPool}) + if err == nil { + t.Fatal("expected error while verifying signature") + } + if !strings.Contains(err.Error(), "certificate signed by unknown authority") { + t.Fatal("expected error while verifying signature") + } + // TODO: Create fake bundle and test verification + if verified == true { + t.Fatalf("expected verified=false, got verified=true") + } +} + func TestValidateAndUnpackCertSuccess(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" @@ -284,6 +387,64 @@ func TestValidateAndUnpackCertSuccessAllowAllValues(t *testing.T) { } } +func TestValidateAndUnpackCertWithSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + rootPool := x509.NewCertPool() + rootPool.AddCert(chain[1]) + co := &CheckOpts{ + RootCerts: rootPool, + } + + // write SCT verification key to disk + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + defer tmpPrivFile.Close() + if _, err := tmpPrivFile.Write([]byte(testdata.LogPublicKeyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + os.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + defer os.Unsetenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") + + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } + + // validate again, explicitly setting enforce SCT + co.EnforceSCT = true + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } +} + +func TestValidateAndUnpackCertWithoutRequiredSCT(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + EnforceSCT: true, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "certificate does not include required embedded SCT") +} + func TestValidateAndUnpackCertInvalidRoot(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" @@ -420,6 +581,95 @@ func TestValidateAndUnpackCertWithChainFailsWithInvalidChain(t *testing.T) { } } +func TestValidateAndUnpackCertWithIdentities(t *testing.T) { + u, err := url.Parse("http://url.example.com") + if err != nil { + t.Fatal("failed to parse url", err) + } + emailSubject := "email@example.com" + dnsSubjects := []string{"dnssubject.example.com"} + ipSubjects := []net.IP{net.ParseIP("1.2.3.4")} + uriSubjects := []*url.URL{u} + oidcIssuer := "https://accounts.google.com" + + tests := []struct { + identities []Identity + wantErrSubstring string + dnsNames []string + emailAddresses []string + ipAddresses []net.IP + uris []*url.URL + }{ + {identities: nil /* No matches required, checks out */}, + {identities: []Identity{ // Strict match on both + {Subject: emailSubject, Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []Identity{ // just issuer + {Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []Identity{ // just subject + {Subject: emailSubject}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []Identity{ // mis-match + {Subject: "wrongsubject", Issuer: oidcIssuer}, + {Subject: emailSubject, Issuer: "wrongissuer"}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: "none of the expected identities matched"}, + {identities: []Identity{ // one good identity, other does not match + {Subject: "wrongsubject", Issuer: "wrongissuer"}, + {Subject: emailSubject, Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []Identity{ // illegal regex for subject + {Subject: "****", Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: "malformed subject in identity"}, + {identities: []Identity{ // illegal regex for issuer + {Subject: emailSubject, Issuer: "****"}}, + wantErrSubstring: "malformed issuer in identity"}, + {identities: []Identity{ // regex matches + {Subject: ".*example.com", Issuer: ".*accounts.google.*"}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []Identity{ // regex matches dnsNames + {Subject: ".*ubject.example.com", Issuer: ".*accounts.google.*"}}, + dnsNames: dnsSubjects, + wantErrSubstring: ""}, + {identities: []Identity{ // regex matches ip + {Subject: "1.2.3.*", Issuer: ".*accounts.google.*"}}, + ipAddresses: ipSubjects, + wantErrSubstring: ""}, + {identities: []Identity{ // regex matches urls + {Subject: ".*url.examp.*", Issuer: ".*accounts.google.*"}}, + uris: uriSubjects, + wantErrSubstring: ""}, + } + for _, tc := range tests { + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCertWithSubjectAlternateNames(tc.dnsNames, tc.emailAddresses, tc.ipAddresses, tc.uris, oidcIssuer, rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + Identities: tc.identities, + } + _, err := ValidateAndUnpackCert(leafCert, co) + if err == nil && tc.wantErrSubstring != "" { + t.Errorf("Expected error %s got none", tc.wantErrSubstring) + } else if err != nil { + if tc.wantErrSubstring == "" { + t.Errorf("Did not expect an error, got err = %v", err) + } else if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Did not get the expected error %s, got err = %v", tc.wantErrSubstring, err) + } + } + } +} func TestCompareSigs(t *testing.T) { // TODO(nsmith5): Add test cases for invalid signature, missing signature etc tests := []struct { @@ -467,10 +717,16 @@ func TestTrustedCertSuccess(t *testing.T) { subPool := x509.NewCertPool() subPool.AddCert(subCert) - err := TrustedCert(leafCert, rootPool, subPool) + chains, err := TrustedCert(leafCert, rootPool, subPool) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } + if len(chains) != 1 { + t.Fatalf("unexpected number of chains found, expected 1, got %v", len(chains)) + } + if len(chains[0]) != 3 { + t.Fatalf("unexpected number of certs in chain, expected 3, got %v", len(chains[0])) + } } func TestTrustedCertSuccessNoIntermediates(t *testing.T) { @@ -480,7 +736,7 @@ func TestTrustedCertSuccessNoIntermediates(t *testing.T) { rootPool := x509.NewCertPool() rootPool.AddCert(rootCert) - err := TrustedCert(leafCert, rootPool, nil) + _, err := TrustedCert(leafCert, rootPool, nil) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } @@ -498,7 +754,7 @@ func TestTrustedCertSuccessChainFromRoot(t *testing.T) { subPool := x509.NewCertPool() subPool.AddCert(subCert) - err := TrustedCert(leafCert, rootPool, subPool) + _, err := TrustedCert(leafCert, rootPool, subPool) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } diff --git a/pkg/policy/attestation.go b/pkg/policy/attestation.go new file mode 100644 index 00000000000..44808201283 --- /dev/null +++ b/pkg/policy/attestation.go @@ -0,0 +1,130 @@ +// +// Copyright 2022 The Sigstore 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 policy + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/pkg/errors" + "github.com/sigstore/cosign/pkg/oci" + + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/pkg/cosign/attestation" +) + +// AttestationToPayloadJSON takes in a verified Attestation (oci.Signature) and +// marshals it into a JSON depending on the payload that's then consumable +// by policy engine like cue, rego, etc. +// +// Anything fed here must have been validated with either +// `VerifyLocalImageAttestations` or `VerifyImageAttestations` +// +// If there's no error, and payload is empty means the predicateType did not +// match the attestation. +func AttestationToPayloadJSON(ctx context.Context, predicateType string, verifiedAttestation oci.Signature) ([]byte, error) { + // Check the predicate up front, no point in wasting time if it's invalid. + predicateURI, ok := options.PredicateTypeMap[predicateType] + if !ok { + return nil, fmt.Errorf("invalid predicate type: %s", predicateType) + } + + var payloadData map[string]interface{} + + p, err := verifiedAttestation.Payload() + if err != nil { + return nil, errors.Wrap(err, "getting payload") + } + + err = json.Unmarshal(p, &payloadData) + if err != nil { + return nil, errors.Wrap(err, "unmarshaling payload data") + } + + var decodedPayload []byte + if val, ok := payloadData["payload"]; ok { + decodedPayload, err = base64.StdEncoding.DecodeString(val.(string)) + if err != nil { + return nil, errors.Wrap(err, "decoding payload") + } + } else { + return nil, fmt.Errorf("could not find payload in payload data") + } + + // Only apply the policy against the requested predicate type + var statement in_toto.Statement + if err := json.Unmarshal(decodedPayload, &statement); err != nil { + return nil, fmt.Errorf("unmarshal in-toto statement: %w", err) + } + if statement.PredicateType != predicateURI { + // This is not the predicate we're looking for, so skip it. + return nil, nil + } + + // NB: In many (all?) of these cases, we could just return the + // 'json.Marshal', but we check for errors here to decorate them + // with more meaningful error message. + var payload []byte + switch predicateType { + case options.PredicateCustom: + payload, err = json.Marshal(statement) + if err != nil { + return nil, errors.Wrap(err, "generating CosignStatement") + } + case options.PredicateLink: + var linkStatement in_toto.LinkStatement + if err := json.Unmarshal(decodedPayload, &linkStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling LinkStatement") + } + payload, err = json.Marshal(linkStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling LinkStatement") + } + case options.PredicateSLSA: + var slsaProvenanceStatement in_toto.ProvenanceStatement + if err := json.Unmarshal(decodedPayload, &slsaProvenanceStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling ProvenanceStatement") + } + payload, err = json.Marshal(slsaProvenanceStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling ProvenanceStatement") + } + case options.PredicateSPDX: + var spdxStatement in_toto.SPDXStatement + if err := json.Unmarshal(decodedPayload, &spdxStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling SPDXStatement") + } + payload, err = json.Marshal(spdxStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling SPDXStatement") + } + case options.PredicateVuln: + var vulnStatement attestation.CosignVulnStatement + if err := json.Unmarshal(decodedPayload, &vulnStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling CosignVulnStatement") + } + payload, err = json.Marshal(vulnStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling CosignVulnStatement") + } + default: + return nil, fmt.Errorf("unsupported predicate type: %s", predicateType) + } + return payload, nil +} diff --git a/pkg/policy/attestation_test.go b/pkg/policy/attestation_test.go new file mode 100644 index 00000000000..c79fb4babda --- /dev/null +++ b/pkg/policy/attestation_test.go @@ -0,0 +1,188 @@ +// +// Copyright 2022 The Sigstore 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 policy + +import ( + "context" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/sigstore/cosign/pkg/cosign/attestation" + "github.com/sigstore/cosign/pkg/cosign/bundle" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" +) + +type failingAttestation struct { +} + +func (fa *failingAttestation) Payload() ([]byte, error) { + return nil, fmt.Errorf("inducing test failure") +} +func (fa *failingAttestation) Annotations() (map[string]string, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Base64Signature() (string, error) { + return "", fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Cert() (*x509.Certificate, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Chain() ([]*x509.Certificate, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Bundle() (*bundle.RekorBundle, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Digest() (v1.Hash, error) { + return v1.Hash{}, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) DiffID() (v1.Hash, error) { + return v1.Hash{}, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Compressed() (io.ReadCloser, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Uncompressed() (io.ReadCloser, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Size() (int64, error) { + return 0, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) MediaType() (types.MediaType, error) { + return types.DockerConfigJSON, fmt.Errorf("unimplemented") +} + +var _ oci.Signature = (*failingAttestation)(nil) + +const ( + // Result of "echo 'nottotostatement' | base64" + // invalidTotoStatement = "bm90dG90b3N0YXRlbWVudAo=" + invalidTotoStatement = `{"payloadType":"application/vnd.in-toto+json","payload":"bm90dG90b3N0YXRlbWVudAo"}` +) + +func checkFailure(t *testing.T, want string, err error) { + t.Helper() + if err == nil { + t.Fatalf("Expected error, got none") + } + if !strings.Contains(err.Error(), want) { + t.Errorf("Failed to get the expected error of %q, got: %s", want, err) + } +} + +func TestFailures(t *testing.T) { + tests := []struct { + payload string + predicateType string + wantErrSubstring string + }{{payload: "", predicateType: "notvalidpredicate", wantErrSubstring: "invalid predicate type"}, + {payload: "", wantErrSubstring: "unmarshaling payload data"}, {payload: "{badness", wantErrSubstring: "unmarshaling payload data"}, + {payload: `{"payloadType":"notmarshallable}`, wantErrSubstring: "unmarshaling payload data"}, + {payload: `{"payload":"shou!ln'twork"}`, wantErrSubstring: "decoding payload"}, + {payload: `{"payloadType":"finebutnopayload"}`, wantErrSubstring: "could not find payload"}, + {payload: invalidTotoStatement, wantErrSubstring: "decoding payload: illegal base64"}, + } + for _, tc := range tests { + att, err := static.NewSignature([]byte(tc.payload), "") + if err != nil { + t.Fatal("Failed to create static.NewSignature: ", err) + } + predicateType := tc.predicateType + if predicateType == "" { + predicateType = "custom" + } + _, err = AttestationToPayloadJSON(context.TODO(), predicateType, att) + checkFailure(t, tc.wantErrSubstring, err) + } +} + +// TestMalformedPayload tests various non-predicate specific failures that +// are done even before we start processing the payload. +// This just stands alone since didn't want to complicate above tests with +// constructing different attestations there. +func TestErroringPayload(t *testing.T) { + // Payload() call fails + _, err := AttestationToPayloadJSON(context.TODO(), "custom", &failingAttestation{}) + checkFailure(t, "inducing test failure", err) +} +func TestAttestationToPayloadJson(t *testing.T) { + dir := "valid" + files := getDirFiles(t, dir) + for _, fileName := range files { + bytes := readAttestationFromTestFile(t, dir, fileName) + ociSig, err := static.NewSignature(bytes, "") + if err != nil { + t.Fatal("Failed to create static.NewSignature: ", err) + } + jsonBytes, err := AttestationToPayloadJSON(context.TODO(), fileName, ociSig) + if err != nil { + t.Fatalf("Failed to convert : %s", err) + } + switch fileName { + case "custom": + var intoto in_toto.Statement + if err := json.Unmarshal(jsonBytes, &intoto); err != nil { + t.Fatal("Wanted custom statement, can't unmarshal to it: ", err) + } + checkPredicateType(t, attestation.CosignCustomProvenanceV01, intoto.PredicateType) + case "vuln": + var vulnStatement attestation.CosignVulnStatement + if err := json.Unmarshal(jsonBytes, &vulnStatement); err != nil { + t.Fatal("Wanted vuln statement, can't unmarshal to it: ", err) + } + checkPredicateType(t, attestation.CosignVulnProvenanceV01, vulnStatement.PredicateType) + case "default": + t.Fatal("non supported predicate file") + } + } +} + +func checkPredicateType(t *testing.T, want, got string) { + t.Helper() + if want != got { + t.Errorf("Did not get expected predicateType, want: %s got: %s", want, got) + } +} + +func readAttestationFromTestFile(t *testing.T, dir, name string) []byte { + t.Helper() + b, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s/%s", dir, name)) + if err != nil { + t.Fatalf("Failed to read file : %s ReadFile() = %s", name, err) + } + return b +} + +func getDirFiles(t *testing.T, dir string) []string { + files, err := ioutil.ReadDir(fmt.Sprintf("testdata/%s", dir)) + if err != nil { + t.Fatalf("Failed to read dir : %s ReadFile() = %s", dir, err) + } + ret := []string{} + for _, file := range files { + ret = append(ret, file.Name()) + } + return ret +} diff --git a/pkg/policy/eval.go b/pkg/policy/eval.go new file mode 100644 index 00000000000..27861b4329d --- /dev/null +++ b/pkg/policy/eval.go @@ -0,0 +1,81 @@ +// +// Copyright 2022 The Sigstore 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 policy + +import ( + "context" + "fmt" + + "cuelang.org/go/cue/cuecontext" + + "knative.dev/pkg/logging" +) + +// EvaluatePolicyAgainstJson is used to run a policy engine against JSON bytes. +// These bytes can be for example Attestations, or ClusterImagePolicy result +// types. +// name - which attestation are we evaluating +// policyType - cue|rego +// policyBody - String representing either cue or rego language +// jsonBytes - Bytes to evaluate against the policyBody in the given language +func EvaluatePolicyAgainstJSON(ctx context.Context, name, policyType string, policyBody string, jsonBytes []byte) error { + logging.FromContext(ctx).Debugf("Evaluating JSON: %s against policy: %s", string(jsonBytes), policyBody) + switch policyType { + case "cue": + cueValidationErr := evaluateCue(ctx, jsonBytes, policyBody) + if cueValidationErr != nil { + return fmt.Errorf("failed evaluating cue policy for %s : %s", name, cueValidationErr.Error()) // nolint + } + case "rego": + regoValidationErr := evaluateRego(ctx, jsonBytes, policyBody) + if regoValidationErr != nil { + return fmt.Errorf("failed evaluating rego policy for type %s", name) + } + default: + return fmt.Errorf("sorry Type %s is not supported yet", policyType) + } + return nil +} + +// evaluateCue evaluates a cue policy `evaluator` against `attestation` +func evaluateCue(ctx context.Context, attestation []byte, evaluator string) error { + logging.FromContext(ctx).Infof("Evaluating attestation: %s", string(attestation)) + logging.FromContext(ctx).Infof("Evaluator: %s", evaluator) + + cueCtx := cuecontext.New() + cueEvaluator := cueCtx.CompileString(evaluator) + if cueEvaluator.Err() != nil { + return fmt.Errorf("failed to compile the cue policy with error: %w", cueEvaluator.Err()) + } + cueAtt := cueCtx.CompileBytes(attestation) + if cueAtt.Err() != nil { + return fmt.Errorf("failed to compile the attestation data with error: %w", cueAtt.Err()) + } + result := cueEvaluator.Unify(cueAtt) + if err := result.Validate(); err != nil { + return fmt.Errorf("failed to evaluate the policy with error: %w", err) + } + return nil +} + +// evaluateRego evaluates a rego policy `evaluator` against `attestation` +func evaluateRego(ctx context.Context, attestation []byte, evaluator string) error { + // TODO(vaikas) Fix this + // The existing stuff wants files, and it doesn't work. There must be + // a way to load it from a []byte like we can do with cue. Tomorrows problem + // regoValidationErrs := rego.ValidateJSON(payload, regoPolicies) + return fmt.Errorf("TODO(vaikas): Don't know how to this from bytes yet") +} diff --git a/pkg/policy/eval_test.go b/pkg/policy/eval_test.go new file mode 100644 index 00000000000..c44729bc265 --- /dev/null +++ b/pkg/policy/eval_test.go @@ -0,0 +1,187 @@ +// +// Copyright 2022 The Sigstore 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 policy + +import ( + "context" + "strings" + "testing" +) + +const ( + customAttestation = ` + { + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "cosign.sigstore.dev/attestation/v1", + "subject": [ + { + "name": "registry.local:5000/cosigned/demo", + "digest": { + "sha256": "416cc82c76114b1744ea58bcbf2f411a0f2de4b0456703bf1bb83d33656951bc" + } + } + ], + "predicate": { + "Data": "foobar e2e test", + "Timestamp": "2022-04-20T18:17:19Z" + } + }` + + vulnAttestation = ` + { + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "cosign.sigstore.dev/attestation/vuln/v1", + "subject": [ + { + "name": "registry.local:5000/cosigned/demo", + "digest": { + "sha256": "416cc82c76114b1744ea58bcbf2f411a0f2de4b0456703bf1bb83d33656951bc" + } + } + ], + "predicate": { + "invocation": { + "parameters": null, + "uri": "invocation.example.com/cosign-testing", + "event_id": "", + "builder.id": "" + }, + "scanner": { + "uri": "fakescanner.example.com/cosign-testing", + "version": "", + "db": { + "uri": "", + "version": "" + }, + "result": null + }, + "metadata": { + "scanStartedOn": "2022-04-12T00:00:00Z", + "scanFinishedOn": "2022-04-12T00:10:00Z" + } + } + }` + + cipAttestation = "{\"authorityMatches\":{\"keyatt\":{\"signatures\":null,\"attestations\":{\"vuln-key\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"keysignature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null},\"keylessatt\":{\"signatures\":null,\"attestations\":{\"custom-keyless\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}}}}" +) + +func TestEvalPolicy(t *testing.T) { + // TODO(vaikas): Consider moving the attestations/cue files into testdata + // directory. + tests := []struct { + name string + json string + policyType string + policyFile string + wantErr bool + wantErrSub string + }{{ + name: "custom attestation, mismatched predicateType", + json: customAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + wantErr: true, + wantErrSub: `conflicting values "cosign.sigstore.dev/attestation/v1" and "cosign.sigstore.dev/attestation/vuln/v1"`, + }, { + name: "custom attestation, predicateType and data checks out", + json: customAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar e2e test"`, + }, { + name: "custom attestation, data mismatch", + json: customAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "invalid data here"`, + wantErr: true, + wantErrSub: `predicate.Data: conflicting values "foobar e2e test" and "invalid data here"`, + }, { + name: "vuln attestation, wrong invocation url", + json: vulnAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + predicate: invocation: uri: "invocation.example.com/wrong-url-here"`, + wantErr: true, + wantErrSub: `conflicting values "invocation.example.com/cosign-testing" and "invocation.example.com/wrong-url-here"`, + }, { + name: "vuln attestation, checks out", + json: vulnAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + predicate: invocation: uri: "invocation.example.com/cosign-testing"`, + }, { + name: "cluster image policy main policy, checks out", + json: cipAttestation, + policyType: "cue", + policyFile: `package sigstore + import "struct" + import "list" + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + keylessatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + }`, + }, { + name: "cluster image policy main policy, fails", + json: cipAttestation, + policyType: "cue", + wantErr: true, + wantErrSub: `failed evaluating cue policy for cluster image policy main policy, fails : failed to evaluate the policy with error: authorityMatches.keylessattMinAttestations: conflicting values 2 and "Error" (mismatched types int and string)`, + policyFile: `package sigstore + import "struct" + import "list" + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + if( len(authorityMatches.keylessatt.attestations) < 2) { + keylessattMinAttestations: 2 + keylessattMinAttestations: "Error" + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + }`, + }} + for _, tc := range tests { + ctx := context.Background() + err := EvaluatePolicyAgainstJSON(ctx, tc.name, tc.policyType, tc.policyFile, []byte(tc.json)) + if tc.wantErr { + if err == nil { + t.Errorf("Did not get an error, wanted %s", tc.wantErrSub) + } else if !strings.Contains(err.Error(), tc.wantErrSub) { + t.Errorf("Unexpected error, want: %s got: %s", tc.wantErrSub, err.Error()) + } + } else { + if !tc.wantErr && err != nil { + t.Errorf("Unexpected error, wanted none, got: %s", err.Error()) + } + } + } +} diff --git a/pkg/policy/testdata/valid/custom b/pkg/policy/testdata/valid/custom new file mode 100644 index 00000000000..d05cabaa931 --- /dev/null +++ b/pkg/policy/testdata/valid/custom @@ -0,0 +1 @@ +{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3YxIiwic3ViamVjdCI6W3sibmFtZSI6InJlZ2lzdHJ5LmxvY2FsOjUwMDAva25hdGl2ZS9kZW1vIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjZjNmZkNmE0MTE1YzZlOTk4ZmYzNTdjZDkxNDY4MDkzMWJiOWE2YzFhN2NkNWY1Y2IyZjVlMWMwOTMyYWI2ZWQifX1dLCJwcmVkaWNhdGUiOnsiRGF0YSI6ImZvb2JhciB0ZXN0IGF0dGVzdGF0aW9uIiwiVGltZXN0YW1wIjoiMjAyMi0wNC0wN1QxOToyMjoyNVoifX0=","signatures":[{"keyid":"","sig":"MEUCIQC/slGQVpRKgw4Jo8tcbgo85WNG/FOJfxcvQFvTEnG9swIgP4LeOmID+biUNwLLeylBQpAEgeV6GVcEpyG6r8LVnfY="}]} diff --git a/pkg/policy/testdata/valid/vuln b/pkg/policy/testdata/valid/vuln new file mode 100644 index 00000000000..2a38ba9d81c --- /dev/null +++ b/pkg/policy/testdata/valid/vuln @@ -0,0 +1 @@ +{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3Z1bG4vdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicmVnaXN0cnkubG9jYWw6NTAwMC9rbmF0aXZlL2RlbW8iLCJkaWdlc3QiOnsic2hhMjU2IjoiM2MxOWFhOTgwYTljNTcwOWEyYzk2YzJkMDc3OWZlYmY2ZTVlNDUzYjkyYjE3MmNlODRjYjg1ZmRhZjY5NTM3MyJ9fV0sInByZWRpY2F0ZSI6eyJpbnZvY2F0aW9uIjp7InBhcmFtZXRlcnMiOm51bGwsInVyaSI6IiIsImV2ZW50X2lkIjoiIiwiYnVpbGRlci5pZCI6IiJ9LCJzY2FubmVyIjp7InVyaSI6IiIsInZlcnNpb24iOiIiLCJkYiI6eyJ1cmkiOiIiLCJ2ZXJzaW9uIjoiIn0sInJlc3VsdCI6bnVsbH0sIm1ldGFkYXRhIjp7InNjYW5TdGFydGVkT24iOiIwMDAxLTAxLTAxVDAwOjAwOjAwWiIsInNjYW5GaW5pc2hlZE9uIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19","signatures":[{"keyid":"","sig":"MEUCIHE9QkUy+d6uFwae0LSH2Fgy99na3jQvaYMU6qj5dzbFAiEA0uKmqGY1ZHoQZsd0BR4Ug0c8d+sHT0hPcxA61o4DKlM="}]} diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go index 7b95067a3af..4fef03bfd3c 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go @@ -17,10 +17,6 @@ package clusterimagepolicy import ( "context" "crypto" - "crypto/ecdsa" - "crypto/x509" - "encoding/json" - "encoding/pem" "fmt" "strings" @@ -30,12 +26,14 @@ import ( clusterimagepolicyreconciler "github.com/sigstore/cosign/pkg/client/injection/reconciler/cosigned/v1alpha1/clusterimagepolicy" webhookcip "github.com/sigstore/cosign/pkg/cosign/kubernetes/webhook/clusterimagepolicy" "github.com/sigstore/cosign/pkg/reconciler/clusterimagepolicy/resources" + corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" corev1listers "k8s.io/client-go/listers/core/v1" + "knative.dev/pkg/logging" "knative.dev/pkg/reconciler" "knative.dev/pkg/system" @@ -44,13 +42,6 @@ import ( sigs "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" - - // Register the provider-specific plugins - _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" - _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" - _ "github.com/sigstore/sigstore/pkg/signature/kms/fake" - _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" - _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) // Reconciler implements clusterimagepolicyreconciler.Interface for @@ -73,24 +64,6 @@ var _ clusterimagepolicyreconciler.Finalizer = (*Reconciler)(nil) // ReconcileKind implements Interface.ReconcileKind. func (r *Reconciler) ReconcileKind(ctx context.Context, cip *v1alpha1.ClusterImagePolicy) reconciler.Event { cipCopy, cipErr := r.inlinePublicKeys(ctx, cip) - if cipErr != nil { - // Handle error here - r.handleCIPError(ctx, cip.Name) - return cipErr - } - - // Converting external CIP to webhook CIP - bytes, err := json.Marshal(&cipCopy.Spec) - if err != nil { - return err - } - - var webhookCIP *webhookcip.ClusterImagePolicy - if err := json.Unmarshal(bytes, &webhookCIP); err != nil { - return err - } - - webhookCIP, cipErr = r.convertKeyData(ctx, webhookCIP) if cipErr != nil { r.handleCIPError(ctx, cip.Name) // Note that we return the error about the Invalid cip here to make @@ -98,6 +71,8 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, cip *v1alpha1.ClusterIma return cipErr } + webhookCIP := webhookcip.ConvertClusterImagePolicyV1alpha1ToWebhook(cipCopy) + // See if the CM holding configs exists existing, err := r.configmaplister.ConfigMaps(system.Namespace()).Get(config.ImagePoliciesConfigName) if err != nil { @@ -149,24 +124,6 @@ func (r *Reconciler) FinalizeKind(ctx context.Context, cip *v1alpha1.ClusterImag return r.removeCIPEntry(ctx, existing, cip.Name) } -// convertKeyData will go through the CIP and try to convert key data -// to ecdsa.PublicKey and store it in the returned CIP -// When PublicKeys are successfully set, the authority key's data will be -// cleared out -func (r *Reconciler) convertKeyData(ctx context.Context, cip *webhookcip.ClusterImagePolicy) (*webhookcip.ClusterImagePolicy, error) { - for _, authority := range cip.Authorities { - if authority.Key != nil && authority.Key.Data != "" { - keys, err := convertAuthorityKeys(ctx, authority.Key.Data) - if err != nil { - return nil, err - } - // When publicKeys are successfully converted, clear out Data - authority.Key.PublicKeys = keys - } - } - return cip, nil -} - func (r *Reconciler) handleCIPError(ctx context.Context, cipName string) { // The CIP is invalid, try to remove CIP from the configmap existing, err := r.configmaplister.ConfigMaps(system.Namespace()).Get(config.ImagePoliciesConfigName) @@ -179,35 +136,6 @@ func (r *Reconciler) handleCIPError(ctx context.Context, cipName string) { } } -func convertAuthorityKeys(ctx context.Context, pubKey string) ([]*ecdsa.PublicKey, error) { - keys := []*ecdsa.PublicKey{} - - logging.FromContext(ctx).Debugf("Got public key: %v", pubKey) - - pems := parsePems([]byte(pubKey)) - for _, p := range pems { - key, err := x509.ParsePKIXPublicKey(p.Bytes) - if err != nil { - return nil, err - } - keys = append(keys, key.(*ecdsa.PublicKey)) - } - return keys, nil -} - -func parsePems(b []byte) []*pem.Block { - p, rest := pem.Decode(b) - if p == nil { - return nil - } - pems := []*pem.Block{p} - - if rest != nil { - return append(pems, parsePems(rest)...) - } - return pems -} - // inlinePublicKeys will go through the CIP and try to read the referenced // secrets, KMS keys and convert them into inlined data. Makes a copy of the CIP // before modifying it and returns the copy. diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go index 61e496cf3ad..ee6e667be52 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go @@ -64,10 +64,10 @@ RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== -----END PUBLIC KEY-----` // This is the patch for replacing a single entry in the ConfigMap - replaceCIPPatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}"}]` + replaceCIPPatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}"}]` // This is the patch for adding an entry for non-existing KMS for cipName2 - addCIP2Patch = `[{"op":"add","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"azure-kms://foo/bar\"}}]}"}]` + addCIP2Patch = `[{"op":"add","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"key\":{\"data\":\"azure-kms://foo/bar\"}}]}"}]` // This is the patch for removing the last entry, leaving just the // configmap objectmeta, no data. @@ -81,11 +81,8 @@ RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== // two entries but only one is being removed. For keyless entry. removeSingleEntryKeylessPatch = `[{"op":"remove","path":"/data/test-cip-2"}]` - // This is the patch for inlined secret for key ref data - inlinedSecretKeyPatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}"}]` - // This is the patch for inlined secret for keyless cakey ref data - inlinedSecretKeylessPatch = `[{"op":"replace","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"keyless\":{\"ca-cert\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}}]}"}]` + inlinedSecretKeylessPatch = `[{"op":"replace","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"keyless\":{\"ca-cert\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}}]}"}]` ) func TestReconcile(t *testing.T) { @@ -475,11 +472,10 @@ func TestReconcile(t *testing.T) { }, }}), ), - makeConfigMapWithTwoEntriesNotPublicKeyFromSecret(), makeSecret(keySecretName, validPublicKeyData), }, - WantPatches: []clientgotesting.PatchActionImpl{ - makePatch(inlinedSecretKeyPatch), + WantCreates: []runtime.Object{ + makeConfigMap(), }, PostConditions: []func(*testing.T, *TableRow){ AssertTrackingSecret(system.Namespace(), keySecretName), @@ -580,7 +576,7 @@ func makeConfigMap() *corev1.ConfigMap { Name: config.ImagePoliciesConfigName, }, Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, + cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"name":"authority-0","key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, }, } } @@ -591,7 +587,7 @@ func patchKMS(ctx context.Context, t *testing.T, kmsKey string) clientgotesting. t.Fatalf("Failed to read KMS key ID %q: %v", kmsKey, err) } - patch := `[{"op":"add","path":"/data","value":{"test-kms-cip":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"` + strings.ReplaceAll(pubKey, "\n", "\\\\n") + `\"}}]}"}}]` + patch := `[{"op":"add","path":"/data","value":{"test-kms-cip":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"key\":{\"data\":\"` + strings.ReplaceAll(pubKey, "\n", "\\\\n") + `\"}}]}"}}]` return clientgotesting.PatchActionImpl{ ActionImpl: clientgotesting.ActionImpl{ @@ -610,7 +606,7 @@ func makeDifferentConfigMap() *corev1.ConfigMap { Name: config.ImagePoliciesConfigName, }, Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"-----BEGIN NOTPUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END NOTPUBLIC KEY-----"}}]}`, + cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"name":"authority-0","key":{"data":"-----BEGIN NOTPUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END NOTPUBLIC KEY-----"}}]}`, }, } } @@ -623,22 +619,7 @@ func makeConfigMapWithTwoEntries() *corev1.ConfigMap { Name: config.ImagePoliciesConfigName, }, Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, - cipName2: "remove me please", - }, - } -} - -// Same as MakeConfigMapWithTwoEntries but the inline data is not the secret -// so we will replace it with the secret data -func makeConfigMapWithTwoEntriesNotPublicKeyFromSecret() *corev1.ConfigMap { - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: system.Namespace(), - Name: config.ImagePoliciesConfigName, - }, - Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"NOT A REAL PUBLIC KEY"}}]}`, + cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"name":"authority-0","key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, cipName2: "remove me please", }, } diff --git a/pkg/sget/sget.go b/pkg/sget/sget.go index f61e23b8080..2eba346d96f 100644 --- a/pkg/sget/sget.go +++ b/pkg/sget/sget.go @@ -91,6 +91,7 @@ func (sg *SecureGet) Do(ctx context.Context) error { fulcioVerified := (co.SigVerifier == nil) co.RootCerts = fulcio.GetRoots() + co.IntermediateCerts = fulcio.GetIntermediates() sp, bundleVerified, err := cosign.VerifyImageSignatures(ctx, ref, co) if err != nil { diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index 3fa1310354e..610d0c25b3a 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -19,8 +19,6 @@ import ( "crypto" "crypto/x509" "fmt" - "os" - "path/filepath" "strings" "github.com/pkg/errors" @@ -35,12 +33,6 @@ import ( "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms" - - // Register the provider-specific plugins - _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" - _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" - _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" - _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) var ( @@ -92,7 +84,7 @@ func VerifierForKeyRef(ctx context.Context, keyRef string, hashAlgorithm crypto. } func loadKey(keyPath string, pf cosign.PassFunc) (signature.SignerVerifier, error) { - kb, err := os.ReadFile(filepath.Clean(keyPath)) + kb, err := blob.LoadFileOrURL(keyPath) if err != nil { return nil, err } @@ -175,10 +167,14 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass if strings.Contains(keyRef, "://") { sv, err := kms.Get(ctx, keyRef, crypto.SHA256) - if err != nil { + if err == nil { + return sv, nil + } + var e *kms.ProviderNotFoundError + if !errors.As(err, &e) { return nil, fmt.Errorf("kms get: %w", err) } - return sv, nil + // ProviderNotFoundError is okay; loadKey handles other URL schemes } return loadKey(keyRef, pf) diff --git a/pkg/signature/keys_test.go b/pkg/signature/keys_test.go index e95d335639e..5f1c0ac4389 100644 --- a/pkg/signature/keys_test.go +++ b/pkg/signature/keys_test.go @@ -108,6 +108,35 @@ func TestPublicKeyFromFileRef(t *testing.T) { } } +func TestPublicKeyFromEnvVar(t *testing.T) { + keys, err := cosign.GenerateKeyPair(pass("whatever")) + if err != nil { + t.Fatalf("failed to generate keypair: %v", err) + } + ctx := context.Background() + + os.Setenv("MY_ENV_VAR", string(keys.PublicBytes)) + defer os.Unsetenv("MY_ENV_VAR") + if _, err := PublicKeyFromKeyRef(ctx, "env://MY_ENV_VAR"); err != nil { + t.Fatalf("PublicKeyFromKeyRef returned error: %v", err) + } +} + +func TestSignerVerifierFromEnvVar(t *testing.T) { + passFunc := pass("whatever") + keys, err := cosign.GenerateKeyPair(passFunc) + if err != nil { + t.Fatalf("failed to generate keypair: %v", err) + } + ctx := context.Background() + + os.Setenv("MY_ENV_VAR", string(keys.PrivateBytes)) + defer os.Unsetenv("MY_ENV_VAR") + if _, err := SignerVerifierFromKeyRef(ctx, "env://MY_ENV_VAR", passFunc); err != nil { + t.Fatalf("SignerVerifierFromKeyRef returned error: %v", err) + } +} + func pass(s string) cosign.PassFunc { return func(_ bool) ([]byte, error) { return []byte(s), nil diff --git a/release/cloudbuild.yaml b/release/cloudbuild.yaml index d303345dcab..bbd5bad0332 100644 --- a/release/cloudbuild.yaml +++ b/release/cloudbuild.yaml @@ -32,17 +32,17 @@ steps: echo "Checking out ${_GIT_TAG}" git checkout ${_GIT_TAG} -- name: 'gcr.io/projectsigstore/cosign:v1.7.1@sha256:7d735456ae0c6489d088981a228b944e8a729c2aa979d824a74e44ab843d6ad2' +- name: 'gcr.io/projectsigstore/cosign:v1.7.2@sha256:ad2985a87622d5934a4bc06a61faadff772e377937e42519af4f506e1b019d1e' dir: "go/src/sigstore/cosign" env: - COSIGN_EXPERIMENTAL=true - TUF_ROOT=/tmp args: - 'verify' - - 'ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3' + - 'ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418' # maybe we can build our own image and use that to be more in a safe side -- name: ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3 +- name: ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418 entrypoint: /bin/sh dir: "go/src/sigstore/cosign" env: @@ -65,7 +65,7 @@ steps: gcloud auth configure-docker \ && make release -- name: ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3 +- name: ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418 entrypoint: 'bash' dir: "go/src/sigstore/cosign" env: diff --git a/test/cert_utils.go b/test/cert_utils.go index 25e444ba95a..e39e84707c5 100644 --- a/test/cert_utils.go +++ b/test/cert_utils.go @@ -23,6 +23,8 @@ import ( "crypto/x509/pkix" "encoding/asn1" "math/big" + "net" + "net/url" "time" ) @@ -144,3 +146,36 @@ func GenerateLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Ce return cert, priv, nil } + +func GenerateLeafCertWithSubjectAlternateNames(dnsNames []string, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) { + certTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + EmailAddresses: emailAddresses, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + IsCA: false, + ExtraExtensions: []pkix.Extension{{ + // OID for OIDC Issuer extension + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + Critical: false, + Value: []byte(oidcIssuer), + }}, + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv) + if err != nil { + return nil, nil, err + } + + return cert, priv, nil +} diff --git a/test/e2e_test.go b/test/e2e_test.go index c62280ba28a..1302b9d8478 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -501,8 +501,8 @@ func TestSignBlob(t *testing.T) { KeyRef: pubKeyPath2, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -514,8 +514,8 @@ func TestSignBlob(t *testing.T) { t.Fatal(err) } // Now verify should work with that one, but not the other - must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) } func TestSignBlobBundle(t *testing.T) { @@ -540,7 +540,7 @@ func TestSignBlobBundle(t *testing.T) { BundlePath: bundlePath, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -553,7 +553,7 @@ func TestSignBlobBundle(t *testing.T) { t.Fatal(err) } // Now verify should work - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) // Now we turn on the tlog and sign again defer setenv(t, options.ExperimentalEnv, "1")() @@ -563,7 +563,7 @@ func TestSignBlobBundle(t *testing.T) { // Point to a fake rekor server to make sure offline verification of the tlog entry works os.Setenv(serverEnv, "notreal") - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) } func TestGenerate(t *testing.T) { diff --git a/test/e2e_test_cluster_image_policy.sh b/test/e2e_test_cluster_image_policy.sh new file mode 100755 index 00000000000..6753758526d --- /dev/null +++ b/test/e2e_test_cluster_image_policy.sh @@ -0,0 +1,282 @@ +#!/usr/bin/env bash +# +# Copyright 2022 The Sigstore 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. + + +set -ex + +if [[ -z "${OIDC_TOKEN}" ]]; then + if [[ -z "${TOKEN_ISSUER}" ]]; then + echo "Must specify either env variable OIDC_TOKEN or TOKEN_ISSUER" + exit 1 + else + export OIDC_TOKEN=`curl -s ${ISSUER_URL}` + fi +fi + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Must specify env variable KO_DOCKER_REPO" + exit 1 +fi + +if [[ -z "${FULCIO_URL}" ]]; then + echo "Must specify env variable FULCIO_URL" + exit 1 +fi + +if [[ -z "${REKOR_URL}" ]]; then + echo "Must specify env variable REKOR_URL" + exit 1 +fi + +if [[ -z "${SIGSTORE_CT_LOG_PUBLIC_KEY_FILE}" ]]; then + echo "must specify env variable SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" + exit 1 +fi + +if [[ "${NON_REPRODUCIBLE}"=="1" ]]; then + echo "creating non-reproducible build by adding a timestamp" + export TIMESTAMP=`date +%s` +else + export TIMESTAMP="TIMESTAMP" +fi + +# Trust our own custom Rekor API +export SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 + +# Publish the first test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world TIMESTAMP") +} +EOF + +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage=`ko publish -B example.com/demo` +echo Created image $demoimage +popd +echo '::endgroup::' + +# Publish the second test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world 2 TIMESTAMP") +} +EOF +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage2=`ko publish -B example.com/demo` +popd +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy with keyless signing' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless.yaml +echo '::endgroup::' + +echo '::group:: Sign demo image' +COSIGN_EXPERIMENTAL=1 ./cosign sign --rekor-url ${REKOR_URL} --fulcio-url ${FULCIO_URL} --force --allow-insecure-registry ${demoimage} --identity-token ${OIDC_TOKEN} +echo '::endgroup::' + +echo '::group:: Verify demo image' +COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: Create test namespace and label for verification' +kubectl create namespace demo-keyless-signing +kubectl label namespace demo-keyless-signing cosigned.sigstore.dev/include=true +echo '::endgroup::' + +echo '::group:: test job success' +# We signed this above, this should work +if ! kubectl create -n demo-keyless-signing job demo --image=${demoimage} ; then + echo Failed to create Job in namespace with matching signature! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup::' + +# We did not sign this, should fail +echo '::group:: test job rejection' +if kubectl create -n demo-keyless-signing job demo2 --image=${demoimage2} ; then + echo Failed to block unsigned Job creation! + exit 1 +else + echo Successfully blocked Job creation with unsigned image +fi +echo '::endgroup::' + +echo '::group:: Add cip with identities that match issuer/subject' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml +# make sure the reconciler has enough time to update the configmap +sleep 5 +echo '::endgroup::' + +# This has correct issuer/subject, so should work +echo '::group:: test job success with identities' +if ! kubectl create -n demo-keyless-signing job demo-identities-works --image=${demoimage} ; then + echo Failed to create Job in namespace with matching issuer/subject! + exit 1 +else + echo Succcessfully created Job with signed image keyless +fi +echo '::endgroup::' + +echo '::group:: Add cip with identities that do not match issuer/subject' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml +# make sure the reconciler has enough time to update the configmap +sleep 5 +echo '::endgroup::' + +echo '::group:: test job block' +if kubectl create -n demo-keyless-signing job demo-identities-works --image=${demoimage} ; then + echo Failed to block Job in namespace with non matching issuer and subject! + exit 1 +else + echo Succcessfully blocked Job with mismatching issuer and subject +fi +echo '::endgroup::' + +echo '::group:: Remove mismatching cip, start fresh for key' +kubectl delete cip --all +sleep 5 +echo '::endgroup::' + +echo '::group:: Generate New Signing Key For Colocated Signature' +COSIGN_PASSWORD="" ./cosign generate-key-pair +mv cosign.key cosign-colocated-signing.key +mv cosign.pub cosign-colocated-signing.pub +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Key Signing' +yq '. | .spec.authorities[0].key.data |= load_str("cosign-colocated-signing.pub")' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Create and label new namespace for verification' +kubectl create namespace demo-key-signing +kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true + +echo '::group:: Verify blocks unsigned with the key' +if kubectl create -n demo-key-signing job demo --image=${demoimage}; then + echo Failed to block unsigned Job creation! + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Sign demoimage with cosign key' +COSIGN_PASSWORD="" ./cosign sign --key cosign-colocated-signing.key --force --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} +echo '::endgroup::' + +echo '::group:: Verify demoimage with cosign key' +./cosign verify --key cosign-colocated-signing.pub --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} +echo '::endgroup::' + +echo '::group:: test job success' +# We signed this above, this should work +if ! kubectl create -n demo-key-signing job demo --image=${demoimage} ; then + echo Failed to create Job in namespace after signing with key! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup:: test job success' + +echo '::group:: test job rejection' +# We did not sign this, should fail +if kubectl create -n demo-key-signing job demo2 --image=${demoimage2} ; then + echo Failed to block unsigned Job creation! + exit 1 +else + echo Successfully blocked Job creation with unsigned image +fi +echo '::endgroup::' + +echo '::group:: Generate New Signing Key For Remote Signature' +COSIGN_PASSWORD="" ./cosign generate-key-pair +mv cosign.key cosign-remote-signing.key +mv cosign.pub cosign-remote-signing.pub +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Remote Public Key But Missing Source' +yq '. | .metadata.name = "image-policy-remote-source" + | .spec.authorities[0].key.data |= load_str("cosign-remote-signing.pub")' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Sign demoimage with cosign remote key' +COSIGN_PASSWORD="" COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign sign --key cosign-remote-signing.key --force --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: Verify demoimage with cosign remote key' +if ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${demoimage}; then + echo "Signature should not have been verified unless COSIGN_REPOSITORY was defined" + exit 1 +fi + +if ! COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${demoimage}; then + echo "Signature should have been verified when COSIGN_REPOSITORY was defined" + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Create test namespace and label for remote key verification' +kubectl create namespace demo-key-remote +kubectl label namespace demo-key-remote cosigned.sigstore.dev/include=true +echo '::endgroup::' + +echo '::group:: Verify with three CIP, one without correct Source set' +if kubectl create -n demo-key-remote job demo --image=${demoimage}; then + echo Failed to block unsigned Job creation! + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Remote Public Key With Source' +yq '. | .metadata.name = "image-policy-remote-source" + | .spec.authorities[0].key.data |= load_str("cosign-remote-signing.pub") + | .spec.authorities[0] += {"source": [{"oci": env(KO_DOCKER_REPO)+"/remote-signature"}]}' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Verify with three CIP, one with correct Source set' +# We signed this above and applied remote signature source location above +if ! kubectl create -n demo-key-remote job demo --image=${demoimage}; then + echo Failed to create Job in namespace without label! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup::' + +echo '::group::' Cleanup +kubectl delete cip --all +kubectl delete ns demo-key-signing demo-keyless-signing demo-key-remote +rm cosign*.key cosign*.pub +echo '::endgroup::' diff --git a/test/e2e_test_cluster_image_policy_with_attestations.sh b/test/e2e_test_cluster_image_policy_with_attestations.sh new file mode 100755 index 00000000000..ce95ff76f2a --- /dev/null +++ b/test/e2e_test_cluster_image_policy_with_attestations.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# +# Copyright 2022 The Sigstore 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. + + +set -ex + +if [[ -z "${OIDC_TOKEN}" ]]; then + if [[ -z "${TOKEN_ISSUER}" ]]; then + echo "Must specify either env variable OIDC_TOKEN or TOKEN_ISSUER" + exit 1 + else + export OIDC_TOKEN=`curl -s ${ISSUER_URL}` + fi +fi + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Must specify env variable KO_DOCKER_REPO" + exit 1 +fi + +if [[ -z "${FULCIO_URL}" ]]; then + echo "Must specify env variable FULCIO_URL" + exit 1 +fi + +if [[ -z "${REKOR_URL}" ]]; then + echo "Must specify env variable REKOR_URL" + exit 1 +fi + +if [[ -z "${SIGSTORE_CT_LOG_PUBLIC_KEY_FILE}" ]]; then + echo "must specify env variable SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" + exit 1 +fi + +if [[ "${NON_REPRODUCIBLE}"=="1" ]]; then + echo "creating non-reproducible build by adding a timestamp" + export TIMESTAMP=`date +%s` +else + export TIMESTAMP="TIMESTAMP" +fi + +# Trust our own custom Rekor API +export SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 + +# To simplify testing failures, use this function to execute a kubectl to create +# our job and verify that the failure is expected. +assert_error() { + local KUBECTL_OUT_FILE="/tmp/kubectl.failure.out" + match="$@" + echo looking for ${match} + kubectl delete job demo -n ${NS} --ignore-not-found=true + if kubectl create -n ${NS} job demo --image=${demoimage} 2> ${KUBECTL_OUT_FILE} ; then + echo Failed to block unsigned Job creation! + exit 1 + else + echo Successfully blocked Job creation with expected error: "${match}" + if ! grep -q "${match}" ${KUBECTL_OUT_FILE} ; then + echo Did not get expected failure message, wanted "${match}", got + cat ${KUBECTL_OUT_FILE} + exit 1 + fi + fi +} + +# Publish test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world TIMESTAMP") +} +EOF + +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage=`ko publish -B example.com/demo` +echo Created image $demoimage +popd +echo '::endgroup::' + +echo '::group:: Create and label new namespace for verification' +kubectl create namespace demo-attestations +kubectl label namespace demo-attestations cosigned.sigstore.dev/include=true +export NS=demo-attestations +echo '::endgroup::' + +echo '::group:: Create CIP that requires keyless signature and custom attestation with policy' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml +# allow things to propagate +sleep 5 +echo '::endgroup::' + +# This image has not been signed at all, so should get auto-reject +echo '::group:: test job rejection' +expected_error='no matching signatures' +assert_error ${expected_error} +echo '::endgroup::' + +echo '::group:: Sign demoimage with keyless' +COSIGN_EXPERIMENTAL=1 ./cosign sign --rekor-url ${REKOR_URL} --fulcio-url ${FULCIO_URL} --force --allow-insecure-registry ${demoimage} --identity-token ${OIDC_TOKEN} +echo '::endgroup::' + +# This image has been signed, but does not have an attestation, so should fail. +echo '::group:: test job rejection' +expected_error='no matching attestations' +assert_error ${expected_error} +echo '::endgroup::' + +# Ok, cool. So attest and it should pass. +echo '::group:: Create one keyless attestation and verify it' +echo -n 'foobar e2e test' > ./predicate-file-custom +COSIGN_EXPERIMENTAL=1 ./cosign attest --predicate ./predicate-file-custom --fulcio-url ${FULCIO_URL} --rekor-url ${REKOR_URL} --allow-insecure-registry --force ${demoimage} --identity-token ${OIDC_TOKEN} + +COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --type=custom --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: test job success' +# We signed this with keyless and it has a keyless attestation, so should +# pass. +export KUBECTL_SUCCESS_FILE="/tmp/kubectl.success.out" +if ! kubectl create -n ${NS} job demo --image=${demoimage} 2> ${KUBECTL_SUCCESS_FILE} ; then + echo Failed to create job with keyless signature and an attestation + cat ${KUBECTL_SUCCESS_FILE} + exit 1 +else + echo Created the job with keyless signature and an attestation +fi +echo '::endgroup::' + +echo '::group:: Generate New Signing Key that we use for key-ful signing' +COSIGN_PASSWORD="" ./cosign generate-key-pair +echo '::endgroup::' + +# Ok, so now we have satisfied the keyless requirements, one signature, one +# custom attestation. Let's now do it for 'keyful' one. +echo '::group:: Create CIP that requires a keyful signature and an attestation' +yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub") | .spec.authorities[1].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key-with-attestations.yaml | kubectl apply -f - +# allow things to propagate +sleep 5 +echo '::endgroup::' + +# This image has been signed with keyless, but does not have a keyful signature +# so should fail +echo '::group:: test job rejection' +expected_error='no matching signatures' +assert_error ${expected_error} +echo '::endgroup::' + +# Sign it with key +echo '::group:: Sign demoimage with key, and add to rekor' +COSIGN_EXPERIMENTAL=1 COSIGN_PASSWORD="" ./cosign sign --key cosign.key --force --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} +echo '::endgroup::' + +echo '::group:: Verify demoimage with cosign key' +COSIGN_EXPERIMENTAL=1 ./cosign verify --key cosign.pub --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +# This image has been signed with key, but does not have a key attestation +# so should fail +echo '::group:: test job rejection' +expected_error='no matching attestations' +assert_error ${expected_error} +echo '::endgroup::' + +# Fine, so create an attestation for it that's different from the keyless one +echo '::group:: create keyful attestation, add add to rekor' +echo -n 'foobar key e2e test' > ./predicate-file-key-custom +COSIGN_EXPERIMENTAL=1 COSIGN_PASSWORD="" ./cosign attest --predicate ./predicate-file-key-custom --rekor-url ${REKOR_URL} --key ./cosign.key --allow-insecure-registry --force ${demoimage} + +COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --key ./cosign.pub --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} +echo '::endgroup::' + +echo '::group:: test job success with key / keyless' +# We signed this with keyless and key and it has a key/keyless attestation, so +# should pass. +if ! kubectl create -n ${NS} job demo2 --image=${demoimage} 2> ${KUBECTL_SUCCESS_FILE} ; then + echo Failed to create job with both key/keyless signatures and attestations + cat ${KUBECTL_SUCCESS_FILE} + exit 1 +else + echo Created the job with keyless/key signature and an attestations +fi +echo '::endgroup::' + +# So at this point, we have two CIP, one that requires keyless/key sig +# and attestations with both. Let's take it up a notch. +# Let's create a policy that requires both a keyless and keyful +# signature on the image, as well as two attestations signed by the keyless and +# one custom attestation that's signed by key. +# Note we have to bake in the inline data from the keys above +echo '::group:: Add cip for two signatures and two attestations' +yq '. | .spec.authorities[1].key.data |= load_str("cosign.pub") | .spec.authorities[3].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml | kubectl apply -f - +# allow things to propagate +sleep 5 +echo '::endgroup::' + +# The CIP policy is the one that should fail now because it doesn't have enough +# attestations +echo '::group:: test job rejection' +expected_error='failed to evaluate the policy with error: authorityMatches.keylessattMinAttestations' +assert_error ${expected_error} +echo '::endgroup::' + +echo '::group:: Create vuln keyless attestation and verify it' +COSIGN_EXPERIMENTAL=1 ./cosign attest --predicate ./test/testdata/attestations/vuln-predicate.json --type=vuln --fulcio-url ${FULCIO_URL} --rekor-url ${REKOR_URL} --allow-insecure-registry --force ${demoimage} --identity-token ${OIDC_TOKEN} + +COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --type=vuln --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: test job success' +# We signed this with key and keyless and it has two keyless attestations and +# it has one key attestation, so it should succeed. +if ! kubectl create -n ${NS} job demo3 --image=${demoimage} 2> ${KUBECTL_SUCCESS_FILE} ; then + echo Failed to create job that has two signatures and 3 attestations + cat ${KUBECTL_SUCCESS_FILE} + exit 1 +fi +echo '::endgroup::' + +echo '::group::' Cleanup +kubectl delete cip --all +kubectl delete ns demo-attestations +rm cosign.key cosign.pub +echo '::endgroup::' diff --git a/test/testdata/attestations/vuln-predicate.json b/test/testdata/attestations/vuln-predicate.json new file mode 100644 index 00000000000..a6b351cb0ab --- /dev/null +++ b/test/testdata/attestations/vuln-predicate.json @@ -0,0 +1,21 @@ +{ + "invocation": { + "parameters": null, + "uri": "invocation.example.com/cosign-testing", + "event_id": "", + "builder.id": "" + }, + "scanner": { + "uri": "fakescanner.example.com/cosign-testing", + "version": "", + "db": { + "uri": "", + "version": "" + }, + "result": null + }, + "metadata": { + "scanStartedOn": "2022-04-12T00:00:00Z", + "scanFinishedOn": "2022-04-12T00:10:00Z" + } +} diff --git a/test/testdata/cosigned/e2e/cip-key-with-attestations.yaml b/test/testdata/cosigned/e2e/cip-key-with-attestations.yaml new file mode 100644 index 00000000000..089dade05ab --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-key-with-attestations.yaml @@ -0,0 +1,48 @@ +# Copyright 2022 The Sigstore 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-key-with-attestations +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - name: verify custom attestation + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZxAfzrQG1EbWyCI8LiSB7YgSFXoI + FNGTyQGKHFc6/H8TQumT9VLS78pUwtv3w7EfKoyFZoP32KrO7nzUy2q6Cw== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - name: custom-match-predicate + predicateType: custom + policy: + type: cue + data: | + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar key e2e test" + - name: verify signature + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZxAfzrQG1EbWyCI8LiSB7YgSFXoI + FNGTyQGKHFc6/H8TQumT9VLS78pUwtv3w7EfKoyFZoP32KrO7nzUy2q6Cw== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-key.yaml b/test/testdata/cosigned/e2e/cip-key.yaml index d4d8334905d..7b7784bacdf 100644 --- a/test/testdata/cosigned/e2e/cip-key.yaml +++ b/test/testdata/cosigned/e2e/cip-key.yaml @@ -26,4 +26,5 @@ spec: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZxAfzrQG1EbWyCI8LiSB7YgSFXoI FNGTyQGKHFc6/H8TQumT9VLS78pUwtv3w7EfKoyFZoP32KrO7nzUy2q6Cw== -----END PUBLIC KEY----- - + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml b/test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml new file mode 100644 index 00000000000..77999559fdd --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml @@ -0,0 +1,40 @@ +# Copyright 2022 The Sigstore 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-keyless-with-attestations +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - name: verify custom attestation + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - name: custom-match-predicate + predicateType: custom + policy: + type: cue + data: | + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar e2e test" + - name: verify signature + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml b/test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml new file mode 100644 index 00000000000..15e21dfad22 --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 The Sigstore 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-keyless-with-identities-mismatch +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - keyless: + url: http://fulcio.fulcio-system.svc + identities: + - issuer: .*kubernetes.securenamespace.* + subject: .*kubernetes.io/namespaces/securenamespace/serviceaccounts/default + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml b/test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml new file mode 100644 index 00000000000..5e67eb42b85 --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 The Sigstore 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-keyless-with-identities +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - keyless: + url: http://fulcio.fulcio-system.svc + identities: + - issuer: .*kubernetes.default.* + subject: .*kubernetes.io/namespaces/default/serviceaccounts/default + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml new file mode 100644 index 00000000000..6e0d32f8866 --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml @@ -0,0 +1,115 @@ +# Copyright 2022 The Sigstore 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-requires-two-signatures-two-attestations +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - name: keylessatt + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - predicateType: custom + name: customkeyless + policy: + type: cue + data: | + import "time" + before: time.Parse(time.RFC3339, "2049-10-09T17:10:27Z") + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: { + Data: "foobar e2e test" + Timestamp: after + scanFinishedOn: after + } + } + - name: keyatt + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOz9FcbJM/oOkC26Wfo9paG2tYGBL + usDLHze93DzgLaAPDsyJrygpVnL9M6SOyfyXEsjpBTUu6uFZqHua8hwAlA== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - name: custom-match-predicate + predicateType: custom + policy: + type: cue + data: | + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar key e2e test" + - name: keylesssignature + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc + - name: keysignature + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOz9FcbJM/oOkC26Wfo9paG2tYGBL + usDLHze93DzgLaAPDsyJrygpVnL9M6SOyfyXEsjpBTUu6uFZqHua8hwAlA== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc + policy: + type: cue + data: | + package sigstore + import "struct" + import "list" + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + if (len(authorityMatches.keylessatt.attestations) < 2) { + keylessattMinAttestations: 2 + keylessattMinAttestations: "Error" + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + } diff --git a/test/testdata/cosigned/invalid/keylessref-with-malformed-issuer.yaml b/test/testdata/cosigned/invalid/keylessref-with-malformed-issuer.yaml new file mode 100644 index 00000000000..7b04359ee99 --- /dev/null +++ b/test/testdata/cosigned/invalid/keylessref-with-malformed-issuer.yaml @@ -0,0 +1,25 @@ +# Copyright 2022 The Sigstore 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. +--- +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy +spec: + images: + - glob: image* + authorities: + - keyless: + identities: + - issuer: **** diff --git a/test/testdata/cosigned/invalid/keylessref-with-malformed-subject.yaml b/test/testdata/cosigned/invalid/keylessref-with-malformed-subject.yaml new file mode 100644 index 00000000000..fbfd5f57288 --- /dev/null +++ b/test/testdata/cosigned/invalid/keylessref-with-malformed-subject.yaml @@ -0,0 +1,25 @@ +# Copyright 2022 The Sigstore 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. +--- +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy +spec: + images: + - glob: image* + authorities: + - keyless: + identities: + - subject: **** diff --git a/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml b/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml index 1d3af5bcf24..eff58f34b06 100644 --- a/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml +++ b/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml @@ -25,6 +25,4 @@ spec: secretRef: name: ca-cert-secret namespace: some-namespace - identities: - - issuer: "issue-details" - subject: "subject-details" + url: http://example.com diff --git a/test/testdata/cosigned/valid/valid-policy-regex.yaml b/test/testdata/cosigned/valid/valid-policy-regex.yaml index a82ae479d84..26f46c05391 100644 --- a/test/testdata/cosigned/valid/valid-policy-regex.yaml +++ b/test/testdata/cosigned/valid/valid-policy-regex.yaml @@ -26,13 +26,58 @@ spec: secretRef: name: ca-cert-secret namespace: some-namespacemak - - keyless: + - name: "keyless signatures" + keyless: + identities: + - issuer: "issue-details" + subject: "subject-details" + - name: "keyless attestations" + keyless: identities: - issuer: "issue-details" subject: "subject-details" + attestations: + - name: custom-predicate-type-validation + predicateType: custom + policy: + type: cue + data: | + import "time" + before: time.Parse(time.RFC3339, "2049-10-09T17:10:27Z") + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: { + Timestamp: after + scanFinishedOn: after + } + } - keyless: identities: - issuer: "issue-details1" + subject: ".*subject.*" + - keyless: + identities: + - issuer: "issue.*" - key: data: | -----BEGIN PUBLIC KEY----- diff --git a/test/testdata/policies/cue-vuln-fails.cue b/test/testdata/policies/cue-vuln-fails.cue new file mode 100644 index 00000000000..57d741abe00 --- /dev/null +++ b/test/testdata/policies/cue-vuln-fails.cue @@ -0,0 +1,25 @@ +import "time" + +// This is after our scan happened +before: time.Parse(time.RFC3339, "2022-04-01T17:10:27Z") +after: time.Parse(time.RFC3339, "2022-03-09T17:10:27Z") + +// The predicateType field must match this string +predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + +predicate: { + invocation: { + // This is the wrong invocation uri + uri: "invocation.example.com/cosign-testing-invalid" + } + scanner: { + // This is the wrong scanner uri + uri: "fakescanner.example.com/cosign-testing-invalid" + } + metadata: { + scanStartedOn: after + scanFinishedOn: after + } +} diff --git a/test/testdata/policies/cue-vuln-works.cue b/test/testdata/policies/cue-vuln-works.cue new file mode 100644 index 00000000000..79ea71080d9 --- /dev/null +++ b/test/testdata/policies/cue-vuln-works.cue @@ -0,0 +1,22 @@ +import "time" + +before: time.Parse(time.RFC3339, "2022-04-15T17:10:27Z") +after: time.Parse(time.RFC3339, "2022-03-09T17:10:27Z") + +// The predicateType field must match this string +predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + +predicate: { + invocation: { + uri: "invocation.example.com/cosign-testing" + } + scanner: { + uri: "fakescanner.example.com/cosign-testing" + } + metadata: { + scanStartedOn: after + scanFinishedOn: after + } +} diff --git a/third_party/VENDOR-LICENSE/github.com/google/trillian/LICENSE b/third_party/VENDOR-LICENSE/github.com/google/trillian/merkle/LICENSE similarity index 100% rename from third_party/VENDOR-LICENSE/github.com/google/trillian/LICENSE rename to third_party/VENDOR-LICENSE/github.com/google/trillian/merkle/LICENSE diff --git a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md index 8943becf19b..09f5eaf2217 100644 --- a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md +++ b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md @@ -45,6 +45,25 @@ The returned response object is an `*http.Response`, the same thing you would usually get from `net/http`. Had the request failed one or more times, the above call would block and retry with exponential backoff. +## Retrying cases that fail after a seeming success + +It's possible for a request to succeed in the sense that the expected response headers are received, but then to encounter network-level errors while reading the response body. In go-retryablehttp's most basic usage, this error would not be retryable, due to the out-of-band handling of the response body. In some cases it may be desirable to handle the response body as part of the retryable operation. + +A toy example (which will retry the full request and succeed on the second attempt) is shown below: + +```go +c := retryablehttp.NewClient() +r := retryablehttp.NewRequest("GET", "://foo", nil) +handlerShouldRetry := true +r.SetResponseHandler(func(*http.Response) error { + if !handlerShouldRetry { + return nil + } + handlerShouldRetry = false + return errors.New("retryable error") +}) +``` + ## Getting a stdlib `*http.Client` with retries It's possible to convert a `*retryablehttp.Client` directly to a `*http.Client`. diff --git a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go index adbdd92e3ba..57116e96072 100644 --- a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go +++ b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go @@ -69,11 +69,21 @@ var ( // scheme specified in the URL is invalid. This error isn't typed // specifically so we resort to matching on the error string. schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`) + + // A regular expression to match the error returned by net/http when the + // TLS certificate is not trusted. This error isn't typed + // specifically so we resort to matching on the error string. + notTrustedErrorRe = regexp.MustCompile(`certificate is not trusted`) ) // ReaderFunc is the type of function that can be given natively to NewRequest type ReaderFunc func() (io.Reader, error) +// ResponseHandlerFunc is a type of function that takes in a Response, and does something with it. +// It only runs if the initial part of the request was successful. +// If an error is returned, the client's retry policy will be used to determine whether to retry the whole request. +type ResponseHandlerFunc func(*http.Response) error + // LenReader is an interface implemented by many in-memory io.Reader's. Used // for automatically sending the right Content-Length header when possible. type LenReader interface { @@ -86,6 +96,8 @@ type Request struct { // used to rewind the request data in between retries. body ReaderFunc + responseHandler ResponseHandlerFunc + // Embed an HTTP request directly. This makes a *Request act exactly // like an *http.Request so that all meta methods are supported. *http.Request @@ -94,8 +106,16 @@ type Request struct { // WithContext returns wrapped Request with a shallow copy of underlying *http.Request // with its context changed to ctx. The provided ctx must be non-nil. func (r *Request) WithContext(ctx context.Context) *Request { - r.Request = r.Request.WithContext(ctx) - return r + return &Request{ + body: r.body, + responseHandler: r.responseHandler, + Request: r.Request.WithContext(ctx), + } +} + +// SetResponseHandler allows setting the response handler. +func (r *Request) SetResponseHandler(fn ResponseHandlerFunc) { + r.responseHandler = fn } // BodyBytes allows accessing the request body. It is an analogue to @@ -252,23 +272,31 @@ func FromRequest(r *http.Request) (*Request, error) { return nil, err } // Could assert contentLength == r.ContentLength - return &Request{bodyReader, r}, nil + return &Request{body: bodyReader, Request: r}, nil } // NewRequest creates a new wrapped request. func NewRequest(method, url string, rawBody interface{}) (*Request, error) { + return NewRequestWithContext(context.Background(), method, url, rawBody) +} + +// NewRequestWithContext creates a new wrapped request with the provided context. +// +// The context controls the entire lifetime of a request and its response: +// obtaining a connection, sending the request, and reading the response headers and body. +func NewRequestWithContext(ctx context.Context, method, url string, rawBody interface{}) (*Request, error) { bodyReader, contentLength, err := getBodyReaderAndContentLength(rawBody) if err != nil { return nil, err } - httpReq, err := http.NewRequest(method, url, nil) + httpReq, err := http.NewRequestWithContext(ctx, method, url, nil) if err != nil { return nil, err } httpReq.ContentLength = contentLength - return &Request{bodyReader, httpReq}, nil + return &Request{body: bodyReader, Request: httpReq}, nil } // Logger interface allows to use other loggers than @@ -435,6 +463,9 @@ func baseRetryPolicy(resp *http.Response, err error) (bool, error) { } // Don't retry if the error was due to TLS cert verification failure. + if notTrustedErrorRe.MatchString(v.Error()) { + return false, v + } if _, ok := v.Err.(x509.UnknownAuthorityError); ok { return false, v } @@ -455,7 +486,7 @@ func baseRetryPolicy(resp *http.Response, err error) (bool, error) { // the server time to recover, as 500's are typically not permanent // errors and may relate to outages on the server side. This will catch // invalid response codes as well, like 0 and 999. - if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != 501) { + if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) { return true, fmt.Errorf("unexpected HTTP status %s", resp.Status) } @@ -555,13 +586,12 @@ func (c *Client) Do(req *Request) (*http.Response, error) { var resp *http.Response var attempt int var shouldRetry bool - var doErr, checkErr error + var doErr, respErr, checkErr error for i := 0; ; i++ { + doErr, respErr = nil, nil attempt++ - var code int // HTTP response code - // Always rewind the request body when non-nil. if req.body != nil { body, err := req.body() @@ -589,19 +619,24 @@ func (c *Client) Do(req *Request) (*http.Response, error) { // Attempt the request resp, doErr = c.HTTPClient.Do(req.Request) - if resp != nil { - code = resp.StatusCode - } // Check if we should continue with retries. shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, doErr) + if !shouldRetry && doErr == nil && req.responseHandler != nil { + respErr = req.responseHandler(resp) + shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, respErr) + } - if doErr != nil { + err := doErr + if respErr != nil { + err = respErr + } + if err != nil { switch v := logger.(type) { case LeveledLogger: - v.Error("request failed", "error", doErr, "method", req.Method, "url", req.URL) + v.Error("request failed", "error", err, "method", req.Method, "url", req.URL) case Logger: - v.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, doErr) + v.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err) } } else { // Call this here to maintain the behavior of logging all requests, @@ -636,11 +671,11 @@ func (c *Client) Do(req *Request) (*http.Response, error) { } wait := c.Backoff(c.RetryWaitMin, c.RetryWaitMax, i, resp) - desc := fmt.Sprintf("%s %s", req.Method, req.URL) - if code > 0 { - desc = fmt.Sprintf("%s (status: %d)", desc, code) - } if logger != nil { + desc := fmt.Sprintf("%s %s", req.Method, req.URL) + if resp != nil { + desc = fmt.Sprintf("%s (status: %d)", desc, resp.StatusCode) + } switch v := logger.(type) { case LeveledLogger: v.Debug("retrying request", "request", desc, "timeout", wait, "remaining", remain) @@ -648,11 +683,13 @@ func (c *Client) Do(req *Request) (*http.Response, error) { v.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain) } } + timer := time.NewTimer(wait) select { case <-req.Context().Done(): + timer.Stop() c.HTTPClient.CloseIdleConnections() return nil, req.Context().Err() - case <-time.After(wait): + case <-timer.C: } // Make shallow copy of http Request so that we can modify its body @@ -662,15 +699,19 @@ func (c *Client) Do(req *Request) (*http.Response, error) { } // this is the closest we have to success criteria - if doErr == nil && checkErr == nil && !shouldRetry { + if doErr == nil && respErr == nil && checkErr == nil && !shouldRetry { return resp, nil } defer c.HTTPClient.CloseIdleConnections() - err := doErr + var err error if checkErr != nil { err = checkErr + } else if respErr != nil { + err = respErr + } else { + err = doErr } if c.ErrorHandler != nil { diff --git a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go index db808105b4b..167953158c8 100644 --- a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go +++ b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go @@ -337,6 +337,11 @@ func ParseString(in interface{}) (string, error) { } func ParseCommaStringSlice(in interface{}) ([]string, error) { + jsonIn, ok := in.(json.Number) + if ok { + in = jsonIn.String() + } + rawString, ok := in.(string) if ok && rawString == "" { return []string{}, nil