Repository: sigstore/sigstore-python Branch: main Commit: 3a19f924b4f5 Files: 207 Total size: 869.0 KB Directory structure: gitextract_rwb714tf/ ├── .gitattributes ├── .github/ │ ├── actions/ │ │ └── upload-coverage/ │ │ └── action.yml │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── check-embedded-root.yml │ ├── ci.yml │ ├── conformance.yml │ ├── cross-os.yml │ ├── cross-version-verify.yaml │ ├── depsreview.yml │ ├── docs.yml │ ├── lint.yml │ ├── pin-requirements.yml │ ├── release.yml │ ├── requirements.yml │ ├── scorecards-analysis.yml │ └── staging-tests.yml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cloudbuild.yaml ├── docs/ │ ├── advanced/ │ │ ├── custom_trust.md │ │ └── offline.md │ ├── api/ │ │ ├── errors.md │ │ ├── hashes.md │ │ ├── index.md │ │ ├── models.md │ │ ├── oidc.md │ │ ├── sign.md │ │ └── verify/ │ │ ├── policy.md │ │ └── verifier.md │ ├── index.md │ ├── installation.md │ ├── policy.md │ ├── scripts/ │ │ └── gen_ref_pages.py │ ├── signing.md │ ├── stylesheets/ │ │ └── custom.css │ └── verify.md ├── install/ │ └── requirements.in ├── mkdocs.yml ├── pyproject.toml ├── sigstore/ │ ├── __init__.py │ ├── __main__.py │ ├── _cli.py │ ├── _internal/ │ │ ├── __init__.py │ │ ├── fulcio/ │ │ │ ├── __init__.py │ │ │ └── client.py │ │ ├── key_details.py │ │ ├── merkle.py │ │ ├── oidc/ │ │ │ ├── __init__.py │ │ │ └── oauth.py │ │ ├── rekor/ │ │ │ ├── __init__.py │ │ │ ├── checkpoint.py │ │ │ ├── client.py │ │ │ └── client_v2.py │ │ ├── sct.py │ │ ├── timestamp.py │ │ ├── trust.py │ │ └── tuf.py │ ├── _store/ │ │ ├── __init__.py │ │ ├── https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/ │ │ │ ├── root.json │ │ │ ├── signing_config.v0.2.json │ │ │ └── trusted_root.json │ │ └── https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/ │ │ ├── root.json │ │ ├── signing_config.v0.2.json │ │ └── trusted_root.json │ ├── _utils.py │ ├── dsse/ │ │ ├── __init__.py │ │ └── _predicate.py │ ├── errors.py │ ├── hashes.py │ ├── models.py │ ├── oidc.py │ ├── py.typed │ ├── sign.py │ └── verify/ │ ├── __init__.py │ ├── policy.py │ └── verifier.py └── test/ ├── assets/ │ ├── a.dsse.staging-rekor-v2.txt │ ├── a.dsse.staging-rekor-v2.txt.sigstore.json │ ├── a.txt │ ├── a.txt.crt │ ├── a.txt.sig │ ├── b.txt │ ├── b.txt.crt │ ├── b.txt.sig │ ├── bad.txt │ ├── bad.txt.crt │ ├── bad.txt.sig │ ├── bundle.txt │ ├── bundle.txt.crt │ ├── bundle.txt.sig │ ├── bundle.txt.sigstore │ ├── bundle_cve_2022_36056.txt │ ├── bundle_cve_2022_36056.txt.sigstore │ ├── bundle_invalid_version.txt │ ├── bundle_invalid_version.txt.sigstore │ ├── bundle_no_cert_v1.txt │ ├── bundle_no_cert_v1.txt.sigstore │ ├── bundle_no_checkpoint.txt │ ├── bundle_no_checkpoint.txt.bundle │ ├── bundle_no_checkpoint.txt.crt │ ├── bundle_no_checkpoint.txt.sigstore │ ├── bundle_no_log_entry.txt │ ├── bundle_no_log_entry.txt.sigstore │ ├── bundle_v3.txt │ ├── bundle_v3.txt.sigstore │ ├── bundle_v3_alt.txt │ ├── bundle_v3_alt.txt.sigstore │ ├── bundle_v3_github.whl │ ├── bundle_v3_github.whl.sigstore │ ├── bundle_v3_no_signed_time.txt │ ├── bundle_v3_no_signed_time.txt.sigstore.json │ ├── c.txt │ ├── c.txt.crt │ ├── c.txt.sig │ ├── integration/ │ │ ├── Python-3.12.5.tgz.sigstore │ │ ├── a.txt │ │ ├── attest/ │ │ │ ├── slsa_predicate_v0_2.json │ │ │ └── slsa_predicate_v1_0.json │ │ ├── b.txt │ │ ├── bundle_v3.txt │ │ ├── bundle_v3.txt.sigstore │ │ └── c.txt │ ├── offline-rekor.txt │ ├── offline-rekor.txt.crt │ ├── offline-rekor.txt.sig │ ├── signing_config/ │ │ ├── signingconfig-only-v1-rekor.v2.json │ │ └── signingconfig.v2.json │ ├── staging-rekor-v2.txt │ ├── staging-rekor-v2.txt.sigstore.json │ ├── staging-tuf/ │ │ ├── 16.snapshot.json │ │ ├── 17.targets.json │ │ ├── targets/ │ │ │ ├── 0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1.fulcio.crt.pem │ │ │ ├── 0f395087486ba318321eda478d847962b1dd89846c7dc6e95752a6b110669393.signing_config.v0.2.json │ │ │ ├── 1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959.rekor.pub │ │ │ ├── 7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6.ctfe_2022_2.pub │ │ │ ├── 782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b.fulcio_intermediate.crt.pem │ │ │ └── ed6a9cf4e7c2e3297a4b5974fce0d17132f03c63512029d7aa3a402b43acab49.trusted_root.json │ │ └── timestamp.json │ ├── trust_config/ │ │ ├── config.badtype.json │ │ └── config.v1.json │ ├── trusted_root/ │ │ ├── certificate_authority.empty.json │ │ ├── certificate_authority.json │ │ ├── certificate_authority.missingroot.json │ │ ├── trustedroot.badtype.json │ │ ├── trustedroot.v1.json │ │ └── trustedroot.v1.local_tlog_ed25519_rekor-tiles.json │ ├── tsa/ │ │ ├── bundle.duplicate.sigstore │ │ ├── bundle.many_timestamp.sigstore │ │ ├── bundle.txt │ │ ├── bundle.txt.late_timestamp.sigstore │ │ ├── bundle.txt.sigstore │ │ ├── ca.json │ │ ├── issue1482-message │ │ ├── issue1482-timestamp-with-no-cert │ │ └── trust_config.json │ └── x509/ │ ├── bogus-intermediate-with-eku.pem │ ├── bogus-intermediate.pem │ ├── bogus-leaf-invalid-eku.pem │ ├── bogus-leaf-invalid-ku.pem │ ├── bogus-leaf-missing-eku.pem │ ├── bogus-leaf.pem │ ├── bogus-root-invalid-ku.pem │ ├── bogus-root-missing-ku.pem │ ├── bogus-root-noncritical-bc.pem │ ├── bogus-root.pem │ ├── build-testcases.py │ ├── nonroot-privkey.pem │ └── root-privkey.pem ├── conftest.py ├── integration/ │ ├── cli/ │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_attest.py │ │ ├── test_plumbing.py │ │ ├── test_sign.py │ │ └── test_verify.py │ └── sigstore-python-conformance └── unit/ ├── __init__.py ├── conftest.py ├── internal/ │ ├── __init__.py │ ├── fulcio/ │ │ ├── __init__.py │ │ └── test_client.py │ ├── oidc/ │ │ ├── __init__.py │ │ └── test_issuer.py │ ├── rekor/ │ │ ├── __init__.py │ │ └── test_client_v2.py │ ├── test_key_details.py │ ├── test_sct.py │ ├── test_timestamping.py │ └── test_trust.py ├── test_dsse.py ├── test_hashes.py ├── test_models.py ├── test_oidc.py ├── test_session_reuse.py ├── test_sign.py ├── test_store.py ├── test_utils.py ├── test_version.py └── verify/ ├── __init__.py ├── test_policy.py └── test_verifier.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # These directories contain TUF and other assets that are either digested # or sized-checked so CRLF normalization breaks them. sigstore/_store/** binary diff=text test/assets/** binary diff=text test/assets/x509/** -binary ================================================ FILE: .github/actions/upload-coverage/action.yml ================================================ # Derived from # Originally authored by the PyCA Cryptography maintainers, and licensed under # the terms of the BSD license: # name: Upload Coverage description: Upload coverage files runs: using: "composite" steps: # FIXME(jl): codecov has the option of including machine information in filename that would solve this unique naming # issue more completely. - run: | COVERAGE_UUID=$(python3 -c "import uuid; print(uuid.uuid4())") echo "COVERAGE_UUID=${COVERAGE_UUID}" >> $GITHUB_OUTPUT if [ -f .coverage ]; then mv .coverage .coverage.${COVERAGE_UUID} fi id: coverage-uuid shell: bash - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-data-${{ steps.coverage-uuid.outputs.COVERAGE_UUID }} include-hidden-files: 'true' path: | .coverage.* *.lcov if-no-files-found: ignore ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: pip directory: / schedule: interval: daily - package-ecosystem: github-actions directory: / schedule: interval: daily open-pull-requests-limit: 99 rebase-strategy: "disabled" groups: actions: patterns: - "*" - package-ecosystem: github-actions directory: .github/actions/upload-coverage/ schedule: interval: daily open-pull-requests-limit: 99 rebase-strategy: "disabled" groups: actions: patterns: - "*" ================================================ FILE: .github/pull_request_template.md ================================================ #### Summary #### Release Note #### Documentation ================================================ FILE: .github/workflows/check-embedded-root.yml ================================================ name: Check embedded root on: workflow_dispatch: schedule: - cron: '13 13 * * 3' jobs: check-embedded-root: runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - name: Setup environment run: make dev - name: Check if embedded root is up-to-date run: | make update-embedded-root git diff --exit-code - if: failure() name: Create an issue if embedded root is not up-to-date uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const repo = context.repo.owner + "/" + context.repo.repo const body = ` The Sigstore [TUF repository](https://tuf-repo-cdn.sigstore.dev/) contents have changed: the data embedded in sigstore-python sources can be updated. This is not urgent but will improve cold-cache performance. Run \`make update-embedded-root\` to update the embedded data. This issue was filed by _${context.workflow}_ [workflow run](${context.serverUrl}/${repo}/actions/runs/${context.runId}). ` const issues = await github.rest.search.issuesAndPullRequests({ q: "label:embedded-root-update+state:open+type:issue+repo:" + repo, }) if (issues.data.total_count > 0) { console.log("Issue for embedded root update exists already.") } else { github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title: "Embedded TUF root is not up-to-date", labels: ["embedded-root-update"], body: body, }) console.log("New issue created.") } ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main - series/* pull_request: schedule: - cron: "0 11 * * *" workflow_dispatch: permissions: {} jobs: test: # Avoid scheduled runs in forks if: github.event_name != 'schedule' || github.repository == 'sigstore/sigstore-python' permissions: # Needed to access the workflow's OIDC identity. id-token: write strategy: matrix: conf: - { py: "3.10", os: "ubuntu-latest" } - { py: "3.11", os: "ubuntu-latest" } - { py: "3.12", os: "ubuntu-latest" } - { py: "3.13", os: "ubuntu-latest" } - { py: "3.14", os: "ubuntu-latest" } # NOTE: We only test Windows and macOS on the latest Python; # these primarily exist to ensure that we don't accidentally # introduce Linux-isms into the development tooling. - { py: "3.14", os: "windows-latest" } - { py: "3.14", os: "macos-latest" } runs-on: ${{ matrix.conf.os }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.conf.py }} allow-prereleases: true cache: "pip" cache-dependency-path: pyproject.toml - name: deps run: make dev SIGSTORE_EXTRA=test - name: test (offline) if: matrix.conf.os == 'ubuntu-latest' run: | # Look at me. I am the captain now. sudo sysctl -w kernel.unprivileged_userns_clone=1 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 # We use `unshare` to "un-share" the default networking namespace, # in effect running the tests as if the host is offline. # This in turn effectively exercises the correctness of our # "online-only" test markers, since any test that's online # but not marked as such will fail. # We also explicitly exclude the integration tests, since these are # always online. unshare --map-root-user --net make test T="test/unit" TEST_ARGS="--skip-online -vv --showlocals" - name: test run: make test TEST_ARGS="-vv --showlocals" # TODO: Refactor this or remove it entirely once there's # a suitable staging TSA instance. - name: test (timestamp-authority) if: ${{ matrix.conf.os == 'ubuntu-latest' }} run: | # Fetch the latest sigstore/timestamp-authority build SIGSTORE_TIMESTAMP_VERSION=$(gh api /repos/sigstore/timestamp-authority/releases --jq '.[0].tag_name') wget https://github.com/sigstore/timestamp-authority/releases/download/${SIGSTORE_TIMESTAMP_VERSION}/timestamp-server-linux-amd64 -O /tmp/timestamp-server chmod +x /tmp/timestamp-server # Run the TSA in background /tmp/timestamp-server serve --port 3000 --disable-ntp-monitoring & export TEST_SIGSTORE_TIMESTAMP_AUTHORITY_URL="http://localhost:3000/api/v1/timestamp" # Ensure Timestamp Authority tests are not skipped by # having pytest show skipped tests and verifying ours are running set -o pipefail make test TEST_ARGS="-m timestamp_authority -rs" | tee output ! grep -q "skipping test that requires a Timestamp Authority" output || (echo "ERROR: Found skip message" && exit 1) env: # Needed for `gh api` above. GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: test (interactive) if: (github.event_name != 'pull_request') || !github.event.pull_request.head.repo.fork run: make test-interactive TEST_ARGS="-vv --showlocals" - uses: ./.github/actions/upload-coverage # only aggregate test coverage over linux-based tests to avoid any OS-specific filesystem information stored in # coverage metadata. if: ${{ matrix.conf.os == 'ubuntu-latest' }} all-tests-pass: if: always() && (github.event_name != 'schedule' || github.repository == 'sigstore/sigstore-python') needs: - test runs-on: ubuntu-latest steps: - name: check test jobs uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} coverage: needs: - test runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: pip install coverage[toml] - name: download coverage data uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: all-artifacts/ - name: combine coverage data id: combinecoverage run: | set +e python -m coverage combine all-artifacts/coverage-data-* echo "## python coverage" >> $GITHUB_STEP_SUMMARY python -m coverage report -m --format=markdown >> $GITHUB_STEP_SUMMARY ================================================ FILE: .github/workflows/conformance.yml ================================================ name: Conformance Tests on: push: branches: - main workflow_dispatch: pull_request: schedule: - cron: "45 7 * * 1,4" permissions: {} jobs: conformance: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - name: install sigstore-python run: python -m pip install . - uses: sigstore/sigstore-conformance@9611941d54398f2e3f6383b6f744442a56d2fb2a # v0.0.26 with: entrypoint: ${{ github.workspace }}/test/integration/sigstore-python-conformance xfail: "test_verify*intoto-with-custom-trust-root] test_verify*managed-key-happy-path] test_verify*managed-key-and-trusted-root]" # see issues 1442, 1244 file-issue-on-failure: needs: [conformance] if: failure() && github.event_name == 'schedule' && github.repository == 'sigstore/sigstore-python' permissions: issues: write # required to file an issue runs-on: ubuntu-latest steps: - name: File an issue for conformance test failure uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title: `[CI] Scheduled conformance test failed`, body: ` A scheduled conformance test failed, see [run details](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}). ` }); ================================================ FILE: .github/workflows/cross-os.yml ================================================ # Copyright 2025 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: Cross-platform sign and verify on: push: branches: - main - series/* pull_request: workflow_dispatch: permissions: {} defaults: run: shell: bash jobs: sign: name: Sign on ${{ matrix.os }} runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false matrix: os: [ubuntu, macos, windows] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - run: pip install . - name: Fetch testing oidc token uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@4a8befcc16064dac9e97f210948d226e5c869bdc # v1.0.0 - name: Sign run: python -m sigstore --staging sign --identity-token $(cat oidc-token.txt) test/assets/a.txt - name: upload signature bundle uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.os }}-bundle path: test/assets/a.txt.sigstore.json if-no-files-found: error retention-days: 1 verify: name: Verify ${{ matrix.signed-with-os }} bundle on ${{ matrix.os }} if: ${{ always() }} # don't stop some verification if one of the signing jobs failed needs: [sign] runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false # Don't cancel other jobs if one fails matrix: os: [ubuntu, macos, windows] signed-with-os: [ubuntu, macos, windows] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - run: pip install . - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.signed-with-os }}-bundle - name: Verify run: | python -m sigstore --staging verify github --verbose \ --cert-identity "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" \ --bundle a.txt.sigstore.json \ test/assets/a.txt ================================================ FILE: .github/workflows/cross-version-verify.yaml ================================================ # Copyright 2025 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: Cross-version verify on: push: branches: - main - series/* pull_request: workflow_dispatch: permissions: {} jobs: sign: name: Sign runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - run: pip install . - name: Fetch testing oidc token uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@4a8befcc16064dac9e97f210948d226e5c869bdc # v1.0.0 - name: Sign run: | touch artifact python -m sigstore --staging sign --bundle artifact-staging-rekor2.sigstore.json --identity-token $(cat oidc-token.txt) --rekor-version=2 artifact python -m sigstore --staging sign --bundle artifact-staging-rekor1.sigstore.json --identity-token $(cat oidc-token.txt) --rekor-version=1 artifact python -m sigstore sign --bundle artifact-prod-rekor1.sigstore.json --identity-token $(cat oidc-token.txt) --rekor-version=1 artifact - name: upload signature bundle uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: bundle path: artifact*.sigstore.json if-no-files-found: error retention-days: 1 verify: name: Verify with ${{ matrix.version }} on ${{ matrix.env }} needs: [sign] runs-on: ubuntu-latest strategy: fail-fast: false # Don't cancel other jobs if one fails matrix: # hand crafted list of old versions we care about version: [3.5.6, 3.6.7, 4.0.0, 4.1.0, 4.2.0] env: [staging, prod] exclude: # exclude staging for versions with https://github.com/sigstore/sigstore-python/issues/1656 - env: staging version: 3.5.6 - env: staging version: 4.0.0 - env: staging version: 4.1.0 steps: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - run: pip install sigstore==${{ matrix.version }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: bundle - run: touch artifact - name: Verify (Rekor v2) # Rekor v2 is currently only available on staging, and only supported on sigstore-python 4.x if: startsWith(matrix.version, '3.') != true && matrix.env == 'staging' env: ENV_OPT: ${{ matrix.env == 'staging' && '--staging' || '' }} BUNDLE: artifact-${{matrix.env}}-rekor2.sigstore.json run: | python -m sigstore $ENV_OPT verify github --verbose \ --cert-identity "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" \ --bundle $BUNDLE \ artifact - name: Verify (Rekor v1) env: ENV_OPT: ${{ matrix.env == 'staging' && '--staging' || '' }} BUNDLE: artifact-${{matrix.env}}-rekor1.sigstore.json run: | python -m sigstore $ENV_OPT verify github --verbose \ --cert-identity "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" \ --bundle $BUNDLE \ artifact ================================================ FILE: .github/workflows/depsreview.yml ================================================ # # 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: 'Dependency Review' on: [pull_request] permissions: contents: read jobs: dependency-review: name: License and Vulnerability Scan uses: sigstore/community/.github/workflows/reusable-dependency-review.yml@9b1b5aca605f92ec5b1bf3681b1e61b3dbc420cc ================================================ FILE: .github/workflows/docs.yml ================================================ name: Documentation on: push: branches: - main permissions: {} jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - name: setup run: | make dev SIGSTORE_EXTRA=doc - name: build docs run: | make doc - name: upload docs artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: ./html/ # This is copied from the official `pdoc` example: # https://github.com/mitmproxy/pdoc/blob/main/.github/workflows/docs.yml # # Deploy the artifact to GitHub pages. # This is a separate job so that only actions/deploy-pages has the necessary permissions. deploy: needs: build if: github.repository == 'sigstore/sigstore-python' runs-on: ubuntu-latest permissions: # NOTE: Needed to push to the repository. pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches: - main pull_request: workflow_dispatch: permissions: {} jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - name: deps run: make dev SIGSTORE_EXTRA=lint - name: lint run: make lint check-readme: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # NOTE: We intentionally check --help rendering against our minimum Python, # since it changes slightly between Python versions. - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.10" cache: "pip" cache-dependency-path: pyproject.toml - name: deps run: make dev - name: check-readme run: make check-readme licenses: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # adapted from Warehouse's bin/licenses - run: | for fn in $(find . -type f -name "*.py"); do if [[ ! "$(head -5 $fn | grep "^ *\(#\|\*\|\/\/\) .* License\(d*\)")" ]]; then echo "${fn} is missing a license" exit 1 fi done x509-testcases: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # NOTE: We intentionally check test certificates against our minimum supported Python. - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.10" cache: "pip" cache-dependency-path: pyproject.toml - name: deps run: make dev - name: ensure testcase generation does not regress run: make gen-x509-testcases all-lints-pass: if: always() needs: - lint - check-readme - licenses - x509-testcases runs-on: ubuntu-latest steps: - name: check lint jobs uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} ================================================ FILE: .github/workflows/pin-requirements.yml ================================================ name: Pin Requirements on: workflow_dispatch: inputs: tag: description: Tag to pin dependencies against. required: false type: string workflow_call: inputs: tag: description: Tag to pin dependencies against. required: false type: string permissions: contents: read jobs: update-pinned-requirements: runs-on: ubuntu-latest permissions: contents: write # Branch creation for PR. outputs: sigstore-release-tag: ${{ steps.get-branch.outputs.sigstore-release-tag }} sigstore-pin-requirements-branch: ${{ steps.get-branch.outputs.sigstore-pin-requirements-branch }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: main # NOTE: Needed for `git describe` below. fetch-depth: 0 fetch-tags: true # NOTE: Needed to push back to the repo. persist-credentials: true - name: Get latest tag run: | latest_tag=$(git describe --tags --abbrev=0) echo "LATEST_TAG=${latest_tag}" >> "${GITHUB_ENV}" - name: Set SIGSTORE_RELEASE_TAG and SIGSTORE_NEW_BRANCH id: get-branch env: INPUT_TAG: "${{ inputs.tag }}" run: | if [[ -n "${INPUT_TAG}" ]]; then effective_tag="${INPUT_TAG}" else effective_tag="${LATEST_TAG}" fi # Environment echo "SIGSTORE_RELEASE_TAG=${effective_tag}" >> "${GITHUB_ENV}" echo "SIGSTORE_NEW_BRANCH=pin-requirements/sigstore/${effective_tag}" >> "${GITHUB_ENV}" # Outputs echo "sigstore-release-tag=${effective_tag}" >> "${GITHUB_OUTPUT}" echo "sigstore-pin-requirements-branch=pin-requirements/sigstore/${effective_tag}" >> "${GITHUB_OUTPUT}" - name: Configure git run: | # Set up committer info. # https://github.com/orgs/community/discussions/26560 git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version-file: install/.python-version cache: "pip" cache-dependency-path: pyproject.toml - name: Install dependencies run: pip install pip-tools - name: Compute version from tag run: | echo "SIGSTORE_RELEASE_VERSION=$(echo "${SIGSTORE_RELEASE_TAG}" | sed 's/^v//')" >> "${GITHUB_ENV}" - name: Update requirements run: | cd install echo "sigstore==${SIGSTORE_RELEASE_VERSION}" > requirements.in pip-compile --allow-unsafe --generate-hashes --upgrade --output-file=requirements.txt requirements.in - name: Commit changes and push to branch run: | git commit --all -s -m "[BOT] install: update pinned requirements" git push -f origin "main:${SIGSTORE_NEW_BRANCH}" test-requirements: needs: update-pinned-requirements uses: ./.github/workflows/requirements.yml with: # We can't use `env` variables in this context. # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability ref: ${{ needs.update-pinned-requirements.outputs.sigstore-pin-requirements-branch }} create-pr: needs: - update-pinned-requirements - test-requirements runs-on: ubuntu-latest permissions: contents: write # Pull Request branch modification. pull-requests: write # Pull Request creation. env: SIGSTORE_RELEASE_TAG: ${{ needs.update-pinned-requirements.outputs.sigstore-release-tag }} SIGSTORE_PIN_REQUIREMENTS_BRANCH: ${{ needs.update-pinned-requirements.outputs.sigstore-pin-requirements-branch }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.SIGSTORE_PIN_REQUIREMENTS_BRANCH }} # NOTE: Needed to push back to the repo. persist-credentials: true - name: Reset remote PR branch run: | git fetch origin main git push -f origin "origin/main:${SIGSTORE_PIN_REQUIREMENTS_BRANCH}" - name: Open pull request env: GH_TOKEN: ${{ github.token }} run: | # Try to create a new PR, or update if it already exists # NOTE: Branch deletion after merge is handled by repository settings PR_TITLE="Update pinned requirements for ${SIGSTORE_RELEASE_TAG}" PR_BODY="Pins dependencies for ." if ! gh pr create \ --title "${PR_TITLE}" \ --body "${PR_BODY}" \ --base main \ --head "${SIGSTORE_PIN_REQUIREMENTS_BRANCH}"; then # PR already exists, update it gh pr edit "${SIGSTORE_PIN_REQUIREMENTS_BRANCH}" \ --title "${PR_TITLE}" \ --body "${PR_BODY}" fi ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: release: types: - published permissions: {} jobs: build: name: Build and sign artifacts runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: # NOTE: We intentionally don't use a cache in the release step, # to reduce the risk of cache poisoning. python-version: "3.x" - name: deps run: python -m pip install -U build - name: build run: python -m build - name: sign run: | mkdir -p smoketest-artifacts # we smoke-test sigstore by installing each of the distributions # we've built in a fresh environment and using each to sign and # verify for itself, using the ambient OIDC identity for dist in dist/*; do dist_base="$(basename "${dist}")" python -m venv smoketest-env ./smoketest-env/bin/python -m pip install "${dist}" # NOTE: signing artifacts currently go in a separate directory, # to avoid confusing the package uploader (which otherwise tries # to upload them to PyPI and fails). Future versions of twine # and the gh-action-pypi-publish action should support these artifacts. ./smoketest-env/bin/python -m \ sigstore sign "${dist}" \ --output-signature smoketest-artifacts/"${dist_base}.sig" \ --output-certificate smoketest-artifacts/"${dist_base}.crt" \ --bundle smoketest-artifacts/"${dist_base}.sigstore" # Verify using `.sig` `.crt` pair; ./smoketest-env/bin/python -m \ sigstore verify identity "${dist}" \ --signature "smoketest-artifacts/${dist_base}.sig" \ --cert "smoketest-artifacts/${dist_base}.crt" \ --cert-oidc-issuer https://token.actions.githubusercontent.com \ --cert-identity ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/release.yml@${GITHUB_REF} # Verify using `.sigstore` bundle; ./smoketest-env/bin/python -m \ sigstore verify identity "${dist}" \ --bundle "smoketest-artifacts/${dist_base}.sigstore" \ --cert-oidc-issuer https://token.actions.githubusercontent.com \ --cert-identity ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/release.yml@${GITHUB_REF} rm -rf smoketest-env done - name: Upload built packages uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: built-packages path: ./dist/ if-no-files-found: warn - name: Upload smoketest-artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: smoketest-artifacts path: smoketest-artifacts/ if-no-files-found: warn generate-provenance: needs: [build] runs-on: ubuntu-latest permissions: id-token: write # To sign the provenance. attestations: write # To persist the attestation files. steps: - name: Download artifacts directories # goes to current working directory uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Generate build provenance uses: actions/attest-build-provenance@v4 with: subject-path: "built-packages/*" release-pypi: needs: [build, generate-provenance] runs-on: ubuntu-latest permissions: # Used to authenticate to PyPI via OIDC. id-token: write steps: - name: Download artifacts directories # goes to current working directory uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: publish uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: packages-dir: built-packages/ release-github: needs: [build, generate-provenance] runs-on: ubuntu-latest permissions: # Needed to upload release assets. contents: write steps: - name: Download artifacts directories # goes to current working directory uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Upload artifacts to github # Confusingly, this action also supports updating releases, not # just creating them. This is what we want here, since we've manually # created the release that triggered the action. uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: # smoketest-artifacts/ contains the signatures and certificates. files: | built-packages/* # Trigger workflow to generate pinned requirements.txt. pin-requirements: permissions: # Needed to create branch and pull request. pull-requests: write contents: write # Workflow depends on uploaded release assets. needs: [release-github] # Only trigger workflow on full releases. if: ${{ !github.event.release.prerelease }} uses: ./.github/workflows/pin-requirements.yml with: tag: ${{ github.ref_name }} ================================================ FILE: .github/workflows/requirements.yml ================================================ name: Test requirements.txt on: push: branches: - main workflow_call: inputs: ref: description: The branch, tag, or revision to test. type: string required: true pull_request: schedule: - cron: "0 12 * * *" permissions: {} jobs: test_requirements: name: requirements.txt / ${{ matrix.python_version }} runs-on: ubuntu-latest env: SIGSTORE_REF: ${{ inputs.ref }} strategy: matrix: python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Populate reference from context if: ${{ env.SIGSTORE_REF == '' }} run: | echo "SIGSTORE_REF=${GITHUB_REF}" >> "${GITHUB_ENV}" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.SIGSTORE_REF }} persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 name: Install Python ${{ matrix.python_version }} with: python-version: ${{ matrix.python_version }} allow-prereleases: true cache: "pip" - name: Run test install run: python -m pip install --require-hashes --no-deps -r install/requirements.txt ================================================ FILE: .github/workflows/scorecards-analysis.yml ================================================ name: Scorecards supply-chain security on: # Only the default branch is supported. workflow_dispatch: # Manual branch_protection_rule: schedule: - cron: '30 4 * * 0' push: branches: [ main ] # Clear default permissions. permissions: {} jobs: analysis: name: Scorecards analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write actions: read contents: read # Needed to access GitHub's OIDC token which ensures the uploaded results integrity. id-token: write steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # Read-only PAT token. To create it, # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. repo_token: ${{ secrets.SCORECARD_TOKEN }} # Publish the results to enable scorecard badges. For more details, see # https://github.com/ossf/scorecard-action#publishing-results. # For private repositories, `publish_results` will automatically be set to `false`, # regardless of the value entered here. publish_results: true # Upload the results as artifacts (optional). - name: "Upload artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: sarif_file: results.sarif ================================================ FILE: .github/workflows/staging-tests.yml ================================================ name: Staging Instance Tests on: push: branches: - main schedule: - cron: "0 */8 * * *" permissions: {} jobs: staging-tests: if: github.event_name != 'schedule' || github.repository == 'sigstore/sigstore-python' runs-on: ubuntu-latest permissions: # Needed to access the workflow's OIDC identity. id-token: write # Needed to create an issue, on failure. issues: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: "pip" cache-dependency-path: pyproject.toml - name: staging tests env: SIGSTORE_LOGLEVEL: DEBUG run: | # This is similar to the "smoketest" that we run during the # release workflow, except that we run against Sigstore's # staging instances instead. # We also don't bother to build distributions. python -m venv staging-env ./staging-env/bin/python -m pip install . # Our signing target is not important here, so we just sign # the README in the repository. ./staging-env/bin/python -m sigstore --verbose --staging sign README.md # Verification also requires a different Rekor instance, so we # also test it. ./staging-env/bin/python -m sigstore --verbose --staging verify identity \ --cert-oidc-issuer https://token.actions.githubusercontent.com \ --cert-identity ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/staging-tests.yml@${GITHUB_REF} \ README.md - name: generate an issue if staging tests fail if: failure() run: | cat <<- EOF > /tmp/staging-instance-issue.md ## Staging instance failure A scheduled test against Sigstore's staging instance has failed. This suggests one of three conditions: * A backwards-incompatible change in a Sigstore component; * A regression in \`sigstore-python\`; * A transient error. The full CI failure can be found here: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/$GITHUB_RUN_ID EOF - name: open an issue if the staging tests fail if: failure() uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0 with: title: "[CI] Integration failure: staging instance" # created in the previous step content-filepath: /tmp/staging-instance-issue.md labels: bug,component:cicd,component:tests assignees: woodruffw,di,tetsuo-cpp ================================================ FILE: .gitignore ================================================ .cache/ env/ pip-wheel-metadata/ *.egg-info/ __pycache__/ .coverage* html/ dist/ .python-version build # Ignore some file types that may litter the root directory *.txt *.crt *.sig *.pem *.sh *.pub *.rekor *.sigstore *.sigstore.json # Don't ignore these files when we intend to include them !sigstore/_store/*.crt !sigstore/_store/*.pem !sigstore/_store/*.pub !test/assets/** !test/assets/staging-tuf/** ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to `sigstore-python` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). All versions prior to 0.9.0 are untracked. ## [Unreleased] ### Fixed * Fixed ~60s hang after completing browser-based OIDC authentication. The OIDC redirect server had incomplete HTTP responses and no connection management, causing a keep-alive deadlock with the browser. ## [4.2.0] ### Fixed * Add state validation to OIDC flow to prevent Cross-site request forgery during OIDC authorization ([GHSA-hm8f-75xx-w2vr](https://github.com/sigstore/sigstore-python/security/advisories/GHSA-hm8f-75xx-w2vr)) * verification now ensures that artifact digest documented in bundle and the real digest match (this is a bundle consistency check: bundle signature was always verified over real digest) ([#1652](https://github.com/sigstore/sigstore-python/pull/1652)) * Fix issue with Signed Certificate Timestamp parsing where extensions were not allowed by sigstore-python ([1657](https://github.com/sigstore/sigstore-python/pull/1657), [1659](https://github.com/sigstore/sigstore-python/pull/1659)) ### Changed * Update supported public key algorithms ([#1604](https://github.com/sigstore/sigstore-python/pull/1604)) * trust: Update embedded TUF root ([#1589](https://github.com/sigstore/sigstore-python/pull/1589)) ### Removed * Removed support for Python 3.9 as it is end-of-life ([#1645](https://github.com/sigstore/sigstore-python/pull/1645)) * Removed unused nonce in Oauth flow ([#1649](https://github.com/sigstore/sigstore-python/pull/1649)) ## [4.1.0] ### Added * cli: Support using other Sigstore instances with `--instance URL`. New instances are trusted with new top level command `trust-instance ROOTFILE`. [#1548](https://github.com/sigstore/sigstore-python/pull/1548) ### Changed * Added cryptography 46 to list of compatible cryptography releases ([#1544](https://github.com/sigstore/sigstore-python/pull/1544)) * Improved error message when verifying bundles with unsupported log entry versions ([#1569](https://github.com/sigstore/sigstore-python/pull/1569)) ### Fixed * cli: Always read/write UTF-8. This fixes an issue on Windows where the platform default encoding was used: the issue has existed for a while, but became more visible with signature bundles that contain rekor2 entries. [#1553](https://github.com/sigstore/sigstore-python/pull/1553) ## [4.0.0] This is a major release with a host of API and functionality changes. The major new feature is Rekor v2 support but many other changes are also included, see list below. ### Added * cli: Add `--rekor-version` to `sign` command arguments: This can be useful if Sigstore instance provides multiple Rekor versions and user wants to override the default choice [#1471](https://github.com/sigstore/sigstore-python/pull/1471) * cli: Support parallel signing. When multiple artifacts are signed, the Rekor requests are submitted in parallel: this is especially useful with Rekor v2. [#1468](https://github.com/sigstore/sigstore-python/pull/1468), [#1478](https://github.com/sigstore/sigstore-python/pull/1478), [#1485](https://github.com/sigstore/sigstore-python/pull/1485) * oidc (API): Allow custom audience claims via API [#1402](https://github.com/sigstore/sigstore-python/pull/1402) * rekor (API): Support Rekor v2 (aka rekor-tiles) in both verification and signing. [#1370](https://github.com/sigstore/sigstore-python/pull/1370), [#1422](https://github.com/sigstore/sigstore-python/pull/1422), [#1432](https://github.com/sigstore/sigstore-python/pull/1432) * trust (API): Make TrustedRoot, SigningConfig and ClientTrustConfig public API [#1496](https://github.com/sigstore/sigstore-python/pull/1496) ### Changed * cli: Improve verify UX when wrong instance is used [#1510](https://github.com/sigstore/sigstore-python/pull/1510) * deps: replace sigstore_protobuf_specs dependency with sigstore-models [#1470](https://github.com/sigstore/sigstore-python/pull/1470) * trust: Update embedded TUF root [#1515](https://github.com/sigstore/sigstore-python/pull/1515) * trust (API): TrustConfig now provides the `production()`and `staging()` helpers. Similar methods were removed from SigningConfig, TrustedRoot, SigningContext and Issuer. Use TrustConfig everywhere in code base. [#1363](https://github.com/sigstore/sigstore-python/pull/1363) * trust (API): support SigningConfig v0.2, remove support for v0.1. The new format now fully defines the sigstore instance the client uses. `SigningConfig` class now has methods to return actual clients (like RekorClient) instead of just URLs for that sigstore instance. The `--trust-config` cli option now expects the trust config to contain a v0.2 SigningConfig. [#1358](https://github.com/sigstore/sigstore-python/pull/1358), [#1407](https://github.com/sigstore/sigstore-python/pull/1407) * trust: Support ed25519 keys in trusted root [#1377](https://github.com/sigstore/sigstore-python/pull/1377) ### Fixed * rekor: resolve circular import of LogEntry [#1458](https://github.com/sigstore/sigstore-python/pull/1458) * rekor: Fix checkpoint signature lookup when there are multiple signatures [#1514](https://github.com/sigstore/sigstore-python/pull/1514) * rekor: Fix entry handling so inclusion promise is optional [#1382](https://github.com/sigstore/sigstore-python/pull/1382) * rekor: Avoid trailing slash in post to /entries [#1366](https://github.com/sigstore/sigstore-python/pull/1366) * sign: fetch TSA timestamps before submitting an entry to Rekor [#1463](https://github.com/sigstore/sigstore-python/pull/1463) * timestamp: Specify sha256 in TSA timestamp request [#1373](https://github.com/sigstore/sigstore-python/pull/1373) * trust: Fail less hard when trusted root contains unknown keys [#1424](https://github.com/sigstore/sigstore-python/pull/1424) * verify: Fix TSA cert chain construction (fixes issue in the case where certificate is not embedded in the timestamp) [#1482](https://github.com/sigstore/sigstore-python/pull/1482) * verify: Use TSA hash algorithm specified in the timestamp (SHA-256, SHA-384 and SHA-512 are supported) [#1385](https://github.com/sigstore/sigstore-python/pull/1385) * verify: Check artifact signing time against all established times [#1381](https://github.com/sigstore/sigstore-python/pull/1381) * verify: Handle unset TSA timestamp validity end [#1368](https://github.com/sigstore/sigstore-python/pull/1368) ## [3.6.6] ### Changed * Improved error message when verifying bundles with rekor v2 entries ([#1565](https://github.com/sigstore/sigstore-python/pull/1565)) * Added cryptography 46 to list of compatible cryptography releases ([#1566](https://github.com/sigstore/sigstore-python/pull/1566)) ## [3.6.5] ### Fixed * Fixed verified time handling so that additional timestamps cannot break otherwise valid signature bundles ([#1492](https://github.com/sigstore/sigstore-python/pull/1492)) ### Changed * Added cryptography 45 to list of compatible cryptography releases ([#1498](https://github.com/sigstore/sigstore-python/pull/1498)) ## [3.6.4] ### Fixed * Bumped the `rfc3161-client` dependency to `>=1.0.3` to fix a security vulnerability ([#1451](https://github.com/sigstore/sigstore-python/pull/1451)) ## [3.6.3] ### Fixed * Verify: Avoid hard failure if trusted root contains unsupported keytypes (as verification may succeed without that key). [#1425](https://github.com/sigstore/sigstore-python/pull/1425) ## [3.6.2] ### Fixed * Fixed issue where a trust root with multiple rekor keys was not considered valid: Now any rekor key listed in the trust root is considered good to verify entries [#1350](https://github.com/sigstore/sigstore-python/pull/1350) ### Changed * Upgraded python-tuf dependency to 6.0: Connections to TUF repository now use system certificates (instead of certifi) and have automatic retries * Updated the embedded TUF root to version 12 ## [3.6.1] ### Fixed * Relaxed the transitive dependency on `cryptography` to allow v43 and v44 to be resolved ([#1251](https://github.com/sigstore/sigstore-python/pull/1251)) ## [3.6.0] ### Added * API: The DSSE `Envelope` class now performs automatic validation ([#1211](https://github.com/sigstore/sigstore-python/pull/1211)) * API: Added `signature` property to `Envelope` class for accessing raw signature bytes ([#1211](https://github.com/sigstore/sigstore-python/pull/1211)) * Signed timestamps embedded in bundles are now automatically verified against Timestamp Authorities provided within the Trusted Root ([#1206] (https://github.com/sigstore/sigstore-python/pull/1206)) * Bundles are now generated with signed timestamps when signing if the Trusted Root contains one or more Timestamp Authorities ([#1216](https://github.com/sigstore/sigstore-python/pull/1216)) ### Removed * Support for "detached" SCTs has been fully removed, aligning sigstore-python with other sigstore clients ([#1236](https://github.com/sigstore/sigstore-python/pull/1236)) ### Fixed * Fixed a CLI parsing bug introduced in 3.5.1 where a warning about verifying legacy bundles was never shown ([#1198](https://github.com/sigstore/sigstore-python/pull/1198)) * Strengthened the requirement that an inclusion promise is present *if* no other source of signed time is present ([#1247](https://github.com/sigstore/sigstore-python/pull/1247)) ## [3.5.3] ### Fixed * Corrective release for [3.5.2] ## [3.5.2] ### Fixed * Pinned `cryptography` dependency strictly to prevent future breakage ## [3.5.1] ### Fixed * Fixed a CLI parsing bug introduced in 3.5.0 when attempting to suppress irrelevant warnings ([#1192](https://github.com/sigstore/sigstore-python/pull/1192)) ## [3.5.0] ### Added * CLI: The `sigstore plumbing update-trust-root` command has been added. Like other plumbing-level commands, this is considered unstable and changes are not subject to our semver policy until explicitly noted ([#1174](https://github.com/sigstore/sigstore-python/pull/1174)) ### Fixed * CLI: Fixed an incorrect warning when verifying detached `.crt`/`.sig` inputs ([#1179](https://github.com/sigstore/sigstore-python/pull/1179)) ## [3.4.0] ### Changed * CLI: When verifying, the `--offline` flag now fully disables all online operations, including routine local TUF repository refreshes ([#1143](https://github.com/sigstore/sigstore-python/pull/1143)) * `sigstore-python`'s minimum supported Python version is now 3.9 ### Fixed * CLI: The `sigstore verify` subcommands now always check for a matching input file, rather than unconditionally falling back to matching on a valid `sha256:...` digest pattern ([#1152](https://github.com/sigstore/sigstore-python/pull/1152)) ## [3.3.0] ### Added * CLI: The `sigstore verify` command now outputs the inner in-toto statement when verifying DSSE envelopes. If verification is successful, the output will be the inner in-toto statement. This allows the user to see the statement's predicate, which `sigstore-python` does not verify and should be verified by the user. * CLI: The `sigstore attest` subcommand has been added. This command is similar to `cosign attest` in that it signs over an artifact and a predicate using a DSSE envelope. This commands requires the user to pass a path to the file containing the predicate, and the predicate type. Currently only the SLSA Provenance v0.2 and v1.0 types are supported. * CLI: The `sigstore verify` command now supports verifying digests. This means that the user can now pass a digest like `sha256:aaaa....` instead of the path to an artifact, and `sigstore-python` will verify it as if it was the artifact with that digest. ## [3.2.0] ### Added * API: `models.Bundle.BundleType` is now a public API ([#1089](https://github.com/sigstore/sigstore-python/pull/1089)) * CLI: The `sigstore plumbing` subcommand hierarchy has been added. This hierarchy is for *developer-only* interactions, such as fixing malformed Sigstore bundles. These subcommands are **not considered stable until explicitly documented as such**. ([#1089](https://github.com/sigstore/sigstore-python/pull/1089)) ### Changed * CLI: The default console logger now emits to `stderr`, rather than `stdout` ([#1089](https://github.com/sigstore/sigstore-python/pull/1089)) ## [3.1.0] ### Added * API: `dsse.StatementBuilder` has been added. It can be used to construct an in-toto `Statement` for subsequent enveloping and signing. This API is public but is **not considered stable until the next major release.** ([#1077](https://github.com/sigstore/sigstore-python/pull/1077)) * API: `dsse.Digest`, `dsse.DigestSet`, and `dsse.Subject` have been added. These types can be used with the `StatementBuilder` API as part of in-toto `Statement` construction. These API are public but are **not considered stable until the next major release.** ([#1078](https://github.com/sigstore/sigstore-python/pull/1078)) ### Changed * API: `verify_dsse` now rejects bundles with DSSE envelopes that have more than one signature, rather than checking all signatures against the same key ([#1062](https://github.com/sigstore/sigstore-python/pull/1062)) ## [3.0.0] Maintainers' note: this is a major release, with significant public API and CLI changes. We **strongly** recommend you read the entries below to fully understand the changes between `2.x` and `3.x`. ### Added * API: `Signer.sign_artifact()` has been added, replacing the removed `Signer.sign()` API * API: `Signer.sign_dsse()` has been added. It takes an in-toto `Statement` as an input, producing a DSSE-formatted signature rather than a "bare" signature ([#804](https://github.com/sigstore/sigstore-python/pull/804)) * API: "v3" Sigstore bundles are now supported during verification ([#901](https://github.com/sigstore/sigstore-python/pull/901)) * API: `Verifier.verify(...)` can now take a `Hashed` as an input, performing signature verification on a pre-computed hash value ([#904](https://github.com/sigstore/sigstore-python/pull/904)) * API: The `sigstore.dsse` module has been been added, including APIs for representing in-toto statements and DSSE envelopes ([#930](https://github.com/sigstore/sigstore-python/pull/930)) * CLI: The `--trust-config` flag has been added as a global option, enabling consistent "BYO PKI" uses of `sigstore` with a single flag ([#1010](https://github.com/sigstore/sigstore-python/pull/1010)) * CLI: The `sigstore verify` subcommands can now verify bundles containing DSSE entries, such as those produced by [GitHub Artifact Attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds) ([#1015](https://github.com/sigstore/sigstore-python/pull/1015)) ### Removed * **BREAKING API CHANGE**: `SigningResult` has been removed. The public signing APIs now return `sigstore.models.Bundle`. * **BREAKING API CHANGE**: `VerificationMaterials` has been removed. The public verification APIs now accept `sigstore.models.Bundle`. * **BREAKING API CHANGE**: `Signer.sign(...)` has been removed. Use either `sign_artifact(...)` or `sign_dsse(...)`, depending on whether you're signing opaque bytes or an in-toto statement. * **BREAKING API CHANGE**: `VerificationResult` has been removed. The public verification and policy APIs now raise `sigstore.errors.VerificationError` on failure. * **BREAKING CLI CHANGE**: The `--rekor-url` and `--fulcio-url` flags have been entirely removed. To configure a custom PKI, use `--trust-config` ([#1010](https://github.com/sigstore/sigstore-python/pull/1010)) ### Changed * **BREAKING API CHANGE**: `Verifier.verify(...)` now takes a `bytes | Hashed` as its verification input, rather than implicitly receiving the input through the `VerificationMaterials` parameter ([#904](https://github.com/sigstore/sigstore-python/pull/904)) * **BREAKING API CHANGE**: `VerificationMaterials.rekor_entry(...)` now takes a `Hashed` parameter to convey the digest used for Rekor entry lookup ([#904](https://github.com/sigstore/sigstore-python/pull/904)) * **BREAKING API CHANGE**: `Verifier.verify(...)` now takes a `sigstore.models.Bundle`, instead of a `VerificationMaterials` ([#937](https://github.com/sigstore/sigstore-python/pull/937)) * **BREAKING CLI CHANGE**: `sigstore sign` now emits `{input}.sigstore.json` by default instead of `{input}.sigstore`, per the client specification ([#1007](https://github.com/sigstore/sigstore-python/pull/1007)) * sigstore-python now requires inclusion proofs in all signing and verification flows, regardless of bundle version of input types. Inputs that do not have an inclusion proof (such as detached materials) cause an online lookup before any further processing is performed ([#937](https://github.com/sigstore/sigstore-python/pull/937)) * sigstore-python now generates "v3" bundles by default during signing ([#937](https://github.com/sigstore/sigstore-python/pull/937)) * CLI: Bundles are now always verified offline. The offline flag has no effect. ([#937](https://github.com/sigstore/sigstore-python/pull/937)) * CLI: "Detached" materials are now always verified online, due to a lack of an inclusion proof. Passing `--offline` with detached materials will cause an error ([#937](https://github.com/sigstore/sigstore-python/pull/937)) * API: `sigstore.transparency` has been removed, and its pre-existing APIs have been re-homed under `sigstore.models` ([#990](https://github.com/sigstore/sigstore-python/pull/990)) * API: `oidc.IdentityToken.expected_certificate_subject` has been renamed to `oidc.IdentityToken.federated_issuer` to better describe what it actually contains. No functional changes have been made to it ([#1016](https://github.com/sigstore/sigstore-python/pull/1016)) * API: `policy.Identity` now takes an **optional** OIDC issuer, rather than a required one ([#1015](https://github.com/sigstore/sigstore-python/pull/1015)) * CLI: `sigstore verify github` now requires `--cert-identity` **or** `--repository`, not just `--cert-identity` ([#1015](https://github.com/sigstore/sigstore-python/pull/1015)) ## [2.1.5] ## Fixed * Backported b32ad1bd (slsa-github-generator upgrade) to make release possible ## [2.1.4] ## Fixed * Pinned `securesystemslib` dependency strictly to prevent future breakage ## [2.1.3] ## Fixed * Loosened a version constraint on the `sigstore-protobuf-specs` dependency, to ease use in testing environments ([#943](https://github.com/sigstore/sigstore-python/pull/943)) ## [2.1.2] This is a corrective release for [2.1.1]. ## [2.1.1] ### Fixed * Fixed an incorrect assumption about Rekor checkpoints that future releases of Rekor will not uphold ([#891](https://github.com/sigstore/sigstore-python/pull/891)) ## [2.1.0] ### Added * CLI: `sigstore verify`'s subcommands now discover `{input}.sigstore.json` by default, in addition to the previous `{input}.sigstore`. The former now takes precedence over the latter, and supplying both results in an error ([#820](https://github.com/sigstore/sigstore-python/pull/820)) ## [2.0.1] ### Fixed * CLI: When using `--certificate-chain`, read as `bytes` instead of `str` as expected by the underlying API ([#796](https://github.com/sigstore/sigstore-python/pull/796)) ## [2.0.0] ### Added * CLI: `sigstore sign` and `sigstore get-identity-token` now support the `--oauth-force-oob` option; which has the same behavior as the preexisting `SIGSTORE_OAUTH_FORCE_OOB` environment variable ([#667](https://github.com/sigstore/sigstore-python/pull/667)) * Version `0.2` of the Sigstore bundle format is now supported ([#705](https://github.com/sigstore/sigstore-python/pull/705)) * API addition: `VerificationMaterials.to_bundle()` is a new public API for producing a standard Sigstore bundle from `sigstore-python`'s internal representation ([#719](https://github.com/sigstore/sigstore-python/pull/719)) * API addition: New method `sign.SigningResult.to_bundle()` allows signing applications to serialize to the bundle format that is already usable in verification with `verify.VerificationMaterials.from_bundle()` ([#765](https://github.com/sigstore/sigstore-python/pull/765)) ### Changed * `sigstore verify` now performs additional verification of Rekor's inclusion proofs by cross-checking them against signed checkpoints ([#634](https://github.com/sigstore/sigstore-python/pull/634)) * A cached copy of the trust bundle is now included with the distribution ([#611](https://github.com/sigstore/sigstore-python/pull/611)) * Stopped emitting .sig and .crt signing outputs by default in `sigstore sign`. Sigstore bundles are now preferred ([#614](https://github.com/sigstore/sigstore-python/pull/614)) * Trust root configuration now assumes that the TUF repository contains a trust bundle, rather than falling back to deprecated individual targets ([#626](https://github.com/sigstore/sigstore-python/pull/626)) * API change: the `sigstore.oidc.IdentityToken` API has been stabilized as a wrapper for OIDC tokens ([#635](https://github.com/sigstore/sigstore-python/pull/635)) * API change: `Signer.sign` now takes a `sigstore.oidc.IdentityToken` for its `identity` argument, rather than a "raw" OIDC token ([#635](https://github.com/sigstore/sigstore-python/pull/635)) * API change: `Issuer.identity_token` now returns a `sigstore.oidc.IdentityToken`, rather than a "raw" OIDC token ([#635](https://github.com/sigstore/sigstore-python/pull/635)) * `sigstore verify` is not longer a backwards-compatible alias for `sigstore verify identity`, as it was during the 1.0 release series ([#642](https://github.com/sigstore/sigstore-python/pull/642)) * API change: the `Signer` API has been broken up into `SigningContext` and `Signer`, allowing a `SigningContext` to create individual `Signer` instances that correspond to a single `IdentityToken`. This new API also enables ephemeral key and certificate reuse across multiple inputs, reducing the number of cryptographic operations and network roundtrips required when signing more than one input ([#645](https://github.com/sigstore/sigstore-python/pull/645)) * `sigstore sign` now uses an ephemeral P-256 keypair, rather than P-384 ([#662](https://github.com/sigstore/sigstore-python/pull/662)) * API change: `RekorClientError` does not try to always parse response content as JSON ([#694](https://github.com/sigstore/sigstore-python/pull/694)) * API change: `LogEntry.inclusion_promise` can now be `None`, but only if `LogEntry.inclusion_proof` is not `None` ([#705](https://github.com/sigstore/sigstore-python/pull/705)) * `sigstore-python`'s minimum supported Python version is now 3.8 ([#745](https://github.com/sigstore/sigstore-python/pull/745)) ### Fixed * Fixed a case where `sigstore verify` would fail to verify an otherwise valid inclusion proof due to an incorrect timerange check ([#633](https://github.com/sigstore/sigstore-python/pull/633)) * Removed an unnecessary and backwards-incompatible parameter from the `sigstore.oidc.detect_credential` API ([#641](https://github.com/sigstore/sigstore-python/pull/641)) * Fixed a case where `sigstore sign` (and `sigstore verify`) could fail while using a private instance due to a missing due to a missing `ExtendedKeyUsage` in the CA. We now enforce the fact that the TBSPrecertificate signer must be a valid CA ([#658](https://github.com/sigstore/sigstore-python/pull/658)) * Fixed a case where identity token retrieval would produce an unhelpful error message ([#767](https://github.com/sigstore/sigstore-python/pull/767)) ## [1.1.2] ### Fixed * Updated the `staging-root.json` for recent changes to the Sigstore staging instance ([#602](https://github.com/sigstore/sigstore-python/pull/602)) * Switched TUF requests to their CDN endpoints, rather than direct GCS access ([#609](https://github.com/sigstore/sigstore-python/pull/609)) ## [1.1.1] ### Added * `sigstore sign` now supports the `--output-directory` flag, which places default outputs in the specified directory. Without this flag, default outputs are placed adjacent to the signing input. ([#627](https://github.com/sigstore/sigstore-python/pull/627)) * The whole test suite can now be run locally with `make test-interactive`. ([#576](https://github.com/sigstore/sigstore-python/pull/576)) Users will be prompted to authenticate with their identity provider twice to generate staging and production OIDC tokens, which are used to test the `sigstore.sign` module. All signing tests need to be completed before token expiry, which is currently 60 seconds after issuance. * Network-related errors from the `sigstore._internal.tuf` module now have better diagnostics. ([#525](https://github.com/sigstore/sigstore-python/pull/525)) ### Changed * Replaced ambient credential detection logic with the `id` package ([#535](https://github.com/sigstore/sigstore-python/pull/535)) * Revamped error diagnostics reporting. All errors with diagnostics now implement `sigstore.errors.Error`. * Trust root materials are now retrieved from a single trust bundle, if it is available via TUF ([#542](https://github.com/sigstore/sigstore-python/pull/542)) * Improved diagnostics around Signed Certificate Timestamp verification failures. ([#555](https://github.com/sigstore/sigstore-python/pull/555)) ### Fixed * Fixed a bug in TUF target handling revealed by changes to the production and staging TUF repos ([#522](https://github.com/sigstore/sigstore-python/pull/522)) ## [1.1.0] ### Added * `sigstore sign` now supports Sigstore bundles, which encapsulate the same state as the default `{input}.crt`, `{input}.sig`, and `{input}.rekor` files combined. The default output for the Sigstore bundle is `{input}.sigstore`; this can be disabled with `--no-bundle` or changed with `--bundle ` ([#465](https://github.com/sigstore/sigstore-python/pull/465)) * `sigstore verify` now supports Sigstore bundles. By default, `sigstore` looks for an `{input}.sigstore`; this can be changed with `--bundle ` or the legacy method of verification can be used instead via the `--signature` and `--certificate` flags ([#478](https://github.com/sigstore/sigstore-python/pull/478)) * `sigstore verify identity` and `sigstore verify github` now support the `--offline` flag, which tells `sigstore` to do offline transparency log entry verification. This option replaces the unstable `--require-rekor-offline` option, which has been removed ([#478](https://github.com/sigstore/sigstore-python/pull/478)) ### Fixed * Constrained our dependency on `pyOpenSSL` to `>= 23.0.0` to prevent a runtime error caused by incompatible earlier versions ([#448](https://github.com/sigstore/sigstore-python/pull/448)) ### Removed * `--rekor-bundle` and `--require-rekor-offline` have been removed entirely, as their functionality have been wholly supplanted by Sigstore bundle support and the new `sigstore verify --offline` flag ([#478](https://github.com/sigstore/sigstore-python/pull/478)) ## [1.0.0] ### Changed * `sigstore.rekor` is now `sigstore.transparency`, and its constituent APIs have been renamed to removed implementation detail references ([#402](https://github.com/sigstore/sigstore-python/pull/402)) * `sigstore.transparency.RekorEntryMissing` is now `LogEntryMissing` ([#414](https://github.com/sigstore/sigstore-python/pull/414)) ### Fixed * The TUF network timeout has been relaxed from 4 seconds to 30 seconds, which should reduce the likelihood of spurious timeout errors in environments like GitHub Actions ([#432](https://github.com/sigstore/sigstore-python/pull/432)) ## [0.10.0] ### Added * `sigstore` now supports the `-v`/`--verbose` flag as an alternative to `SIGSTORE_LOGLEVEL` for debug logging ([#372](https://github.com/sigstore/sigstore-python/pull/372)) * The `sigstore verify identity` has been added, and is functionally equivalent to the existing `sigstore verify` subcommand. `sigstore verify` is unchanged, but will be marked deprecated in a future stable version of `sigstore-python` ([#379](https://github.com/sigstore/sigstore-python/pull/379)) * `sigstore` now has a public, importable Python API! You can find its documentation [here](https://sigstore.github.io/sigstore-python/) ([#383](https://github.com/sigstore/sigstore-python/pull/383)) * `sigstore --staging` is now the intended way to request Sigstore's staging instance, rather than per-subcommand options like `sigstore sign --staging`. The latter is unchanged, but will be marked deprecated in a future stable version of `sigstore-python` ([#383](https://github.com/sigstore/sigstore-python/pull/383)) * The per-subcommand options `--rekor-url` and `--rekor-root-pubkey` have been moved to the top-level `sigstore` command. Their subcommand forms are unchanged and will continue to work, but will be marked deprecated in a future stable version of `sigstore-python` ([#381](https://github.com/sigstore/sigstore-python/pull/383)) * `sigstore verify github` has been added, allowing for verification of GitHub-specific claims within given certificate(s) ([#381](https://github.com/sigstore/sigstore-python/pull/381)) ### Changed * The default behavior of `SIGSTORE_LOGLEVEL` has changed; the logger configured is now the `sigstore.*` hierarchy logger, rather than the "root" logger ([#372](https://github.com/sigstore/sigstore-python/pull/372)) * The caching mechanism used for TUF has been changed slightly, to use more future-proof paths ([#373](https://github.com/sigstore/sigstore-python/pull/373)) ### Fixed * Fulcio certificate handling now includes "inactive" but still valid certificates, allowing users to verify older signatures without custom certificate chains ([#386](https://github.com/sigstore/sigstore-python/pull/386)) ## [0.9.0] ### Added * `sigstore verify` now supports `--certificate-chain` and `--rekor-url` during verification. Ordinary uses (i.e. the default or `--staging`) are not affected ([#323](https://github.com/sigstore/sigstore-python/pull/323)) ### Changed * `sigstore sign` and `sigstore verify` now stream their input, rather than consuming it into a single buffer ([#329](https://github.com/sigstore/sigstore-python/pull/329)) * A series of Python 3.11 deprecation warnings were eliminated ([#341](https://github.com/sigstore/sigstore-python/pull/341)) * The "splash" page presented to users during the OAuth flow has been updated to reflect the user-friendly page added to `cosign` ([#356](https://github.com/sigstore/sigstore-python/pull/356)) * `sigstore` now uses TUF to retrieve its trust material for Fulcio and Rekor, replacing the material that was previously baked into `sigstore._store` ([#351](https://github.com/sigstore/sigstore-python/pull/351)) ### Removed * CLI: The `--certificate-chain`, `--rekor-root-pubkey` and `-ctfe` flags have been entirely removed ([#936](https://github.com/sigstore/sigstore-python/pull/936)) [Unreleased]: https://github.com/sigstore/sigstore-python/compare/v4.2.0...HEAD [4.2.0]: https://github.com/sigstore/sigstore-python/compare/v4.1.0...v4.2.0 [4.1.0]: https://github.com/sigstore/sigstore-python/compare/v4.0.0...v4.1.0 [4.0.0]: https://github.com/sigstore/sigstore-python/compare/v3.6.5...v4.0.0 [3.6.5]: https://github.com/sigstore/sigstore-python/compare/v3.6.4...v3.6.5 [3.6.4]: https://github.com/sigstore/sigstore-python/compare/v3.6.3...v3.6.4 [3.6.3]: https://github.com/sigstore/sigstore-python/compare/v3.6.2...v3.6.3 [3.6.2]: https://github.com/sigstore/sigstore-python/compare/v3.6.1...v3.6.2 [3.6.1]: https://github.com/sigstore/sigstore-python/compare/v3.6.0...v3.6.1 [3.6.0]: https://github.com/sigstore/sigstore-python/compare/v3.5.3...v3.6.0 [3.5.3]: https://github.com/sigstore/sigstore-python/compare/v3.5.2...v3.5.3 [3.5.2]: https://github.com/sigstore/sigstore-python/compare/v3.5.1...v3.5.2 [3.5.1]: https://github.com/sigstore/sigstore-python/compare/v3.5.0...v3.5.1 [3.5.0]: https://github.com/sigstore/sigstore-python/compare/v3.4.0...v3.5.0 [3.4.0]: https://github.com/sigstore/sigstore-python/compare/v3.3.0...v3.4.0 [3.3.0]: https://github.com/sigstore/sigstore-python/compare/v3.2.0...v3.3.0 [3.2.0]: https://github.com/sigstore/sigstore-python/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/sigstore/sigstore-python/compare/v3.0.0...v3.1.0 [3.0.0]: https://github.com/sigstore/sigstore-python/compare/v2.1.5...v3.0.0 [2.1.5]: https://github.com/sigstore/sigstore-python/compare/v2.1.4...v2.1.5 [2.1.4]: https://github.com/sigstore/sigstore-python/compare/v2.1.3...v2.1.4 [2.1.3]: https://github.com/sigstore/sigstore-python/compare/v2.1.2...v2.1.3 [2.1.2]: https://github.com/sigstore/sigstore-python/compare/v2.1.1...v2.1.2 [2.1.1]: https://github.com/sigstore/sigstore-python/compare/v2.1.0...v2.1.1 [2.1.0]: https://github.com/sigstore/sigstore-python/compare/v2.0.1...v2.1.0 [2.0.1]: https://github.com/sigstore/sigstore-python/compare/v2.0.0...v2.0.1 [2.0.0]: https://github.com/sigstore/sigstore-python/compare/v1.1.2...v2.0.0 [1.1.2]: https://github.com/sigstore/sigstore-python/compare/v1.1.1...v1.1.2 [1.1.1]: https://github.com/sigstore/sigstore-python/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/sigstore/sigstore-python/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/sigstore/sigstore-python/compare/v0.10.0...v1.0.0 [0.10.0]: https://github.com/sigstore/sigstore-python/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/sigstore/sigstore-python/compare/v0.8.3...v0.9.0 ================================================ FILE: CODEOWNERS ================================================ @sigstore/codeowners-sigstore-python # The CODEOWNERS are managed via a GitHub team, but the current list is (in alphabetical order): # di # tetsuo-cpp # woodruffw ================================================ FILE: CONTRIBUTING.md ================================================ Contributing to sigstore ======================== Thank you for your interest in contributing to `sigstore`! The information below will help you set up a local development environment, as well as performing common development tasks. ## Requirements `sigstore`'s only development environment requirement *should* be Python 3.10 or newer. Development and testing is actively performed on macOS and Linux, but Windows and other supported platforms that are supported by Python should also work. If you're on a system that has GNU Make, you can use the convenience targets included in the `Makefile` that comes in the `sigstore` repository detailed below. But this isn't required; all steps can be done without Make. ## Development steps First, clone this repository: ```bash git clone https://github.com/sigstore/sigstore-python cd sigstore ``` Then, use one of the `Makefile` targets to run a task. The first time this is run, this will also set up the local development virtual environment, and will install `sigstore` as an editable package into this environment. Any changes you make to the `sigstore` source tree will take effect immediately in the virtual environment. ### Linting You can lint locally with: ```bash make lint ``` `sigstore` is automatically linted and formatted with a collection of tools: * [`ruff`](https://github.com/charliermarsh/ruff): Code formatting, PEP-8 linting, style enforcement * [`mypy`](https://mypy.readthedocs.io/en/stable/): Static type checking * [`bandit`](https://github.com/PyCQA/bandit): Security issue scanning * [`interrogate`](https://interrogate.readthedocs.io/en/latest/): Documentation coverage To automatically apply any lint-suggested changes, you can run: ```bash make reformat ``` ### Testing You can run the tests locally with: ```bash make test ``` or: ```bash make test-interactive ``` to run tests that require OIDC credentials (will prompt for authentication to generate tokens). Note that `test-interactive` may fail if you have a slow network, as the tokens generated are only valid for 60 seconds after their issuance. You can also filter by a pattern (uses `pytest -k`): ```bash make test TESTS=test_version ``` To test a specific file: ```bash make test T=path/to/file.py ``` `sigstore` has a [`pytest`](https://docs.pytest.org/)-based unit test suite, including code coverage with [`coverage.py`](https://coverage.readthedocs.io/). #### X.509 test cases `sigstore` includes some checked-in X.509 test assets under [`test/unit/assets/x509`](./test/unit/assets/x509/). These assets are generated by the adjacent [`build-testcases.py`](./test/unit/assets/x509/build-testcases.py) script, which can be updated to generate additional test cases. To re-build the X.509 test cases, you can use `make`: ```bash make gen-x509-testcases ``` ### Documentation If you're running Python 3.10 or newer, you can run the documentation build locally: ```bash make doc ``` `sigstore` uses [`pdoc`](https://github.com/mitmproxy/pdoc) to generate HTML documentation for the public Python APIs. ### Releasing **NOTE**: If you're a non-maintaining contributor, you don't need the steps here! They're documented for completeness and for onboarding future maintainers. Releases of `sigstore` are managed with [`bump`](https://github.com/di/bump) and GitHub Actions. ```bash # default release (patch bump) make release # override the default # vX.Y.Z -> vX.Y.Z-rc.0 make release BUMP_ARGS="--pre rc.0" # vX.Y.Z -> vN.0.0 make release BUMP_ARGS="--major" ``` `make release` will fail if there are any untracked changes in the source tree. If `make release` succeeds, you'll see an output like this: ``` RUN ME MANUALLY: git push origin main && git push origin vX.Y.Z ``` Run that last command sequence to complete the release. ## Development practices Here are some guidelines to follow if you're working on a new feature or changes to `sigstore`'s internal APIs: * *Keep the `sigstore` APIs as private as possible*. Nearly all of `sigstore`'s APIs should be private and treated as unstable and unsuitable for public use. If you're adding a new module to the source tree, prefix the filename with an underscore to emphasize that it's an internal (e.g., `sigstore/_foo.py` instead of `sigstore/foo.py`). * *Perform judicious debug logging.* `sigstore` uses the standard Python [`logging`](https://docs.python.org/3/library/logging.html) module. Use `logger.debug` early and often -- users who experience errors can submit better bug reports when their debug logs include helpful context! * *Update the [CHANGELOG](./CHANGELOG.md)*. If your changes are public or result in changes to `sigstore`'s CLI, please record them under the "Unreleased" section, with an entry in an appropriate subsection ("Added", "Changed", "Removed", or "Fixed"). * Ensure your commits are signed off, as sigstore uses the [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin). You can do it using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: Makefile ================================================ SHELL := /bin/bash PY_MODULE := sigstore ALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py') \ $(shell find test -name '*.py') \ $(shell find docs/scripts -name '*.py') \ # Optionally overridden by the user, if they're using a virtual environment manager. VENV ?= env # On Windows, venv scripts/shims are under `Scripts` instead of `bin`. VENV_BIN := $(VENV)/bin ifeq ($(OS),Windows_NT) VENV_BIN := $(VENV)/Scripts endif # Optionally overridden by the user in the `release` target. BUMP_ARGS := # Optionally overridden by the user in the `test` target. TESTS ?= # Optionally overridden by the user/CI, to limit the installation to a specific # subset of development dependencies. SIGSTORE_EXTRA := dev # If the user selects a specific test pattern to run, set `pytest` to fail fast # and only run tests that match the pattern. # Otherwise, run all tests and enable coverage assertions, since we expect # complete test coverage. ifneq ($(TESTS),) TEST_ARGS := -x -k $(TESTS) $(TEST_ARGS) COV_ARGS := else TEST_ARGS := $(TEST_ARGS) # TODO: Re-enable coverage testing # COV_ARGS := --fail-under 100 endif ifneq ($(T),) T := $(T) else T := test/unit test/integration endif .PHONY: all all: @echo "Run my targets individually!" $(VENV)/pyvenv.cfg: pyproject.toml # Create our Python 3 virtual environment python3 -m venv $(VENV) $(VENV_BIN)/python -m pip install --upgrade pip $(VENV_BIN)/python -m pip install -e .[$(SIGSTORE_EXTRA)] .PHONY: dev dev: $(VENV)/pyvenv.cfg .PHONY: run run: $(VENV)/pyvenv.cfg @. $(VENV_BIN)/activate && sigstore $(ARGS) .PHONY: lint lint: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ ruff format --check $(ALL_PY_SRCS) && \ ruff check $(ALL_PY_SRCS) && \ mypy $(PY_MODULE) && \ bandit -c pyproject.toml -r $(PY_MODULE) && \ python docs/scripts/gen_ref_pages.py --check .PHONY: reformat reformat: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ ruff check --fix $(ALL_PY_SRCS) && \ ruff format $(ALL_PY_SRCS) .PHONY: test test: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ $(TEST_ENV) pytest --cov-append --cov=$(PY_MODULE) $(T) $(TEST_ARGS) && \ python -m coverage report -m $(COV_ARGS) .PHONY: test-interactive test-interactive: TEST_ENV += \ SIGSTORE_IDENTITY_TOKEN_production=$$($(MAKE) -s run ARGS="get-identity-token") \ SIGSTORE_IDENTITY_TOKEN_staging=$$($(MAKE) -s run ARGS="--staging get-identity-token") test-interactive: test .PHONY: gen-x509-testcases gen-x509-testcases: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ export TESTCASE_OVERWRITE=1 && \ python test/assets/x509/build-testcases.py && \ git diff --exit-code .PHONY: doc doc: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ python docs/scripts/gen_ref_pages.py --overwrite && \ mkdocs build --strict --site-dir html .PHONY: package package: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ python3 -m build .PHONY: release release: $(VENV)/pyvenv.cfg @. $(VENV_BIN)/activate && \ NEXT_VERSION=$$(bump $(BUMP_ARGS)) && \ git add $(PY_MODULE)/_version.py && git diff --quiet --exit-code && \ git commit -m "version: v$${NEXT_VERSION}" && \ git tag v$${NEXT_VERSION} && \ echo "RUN ME MANUALLY: git push origin main && git push origin v$${NEXT_VERSION}" .PHONY: check-readme check-readme: # sigstore --help @diff \ <( \ awk '/@begin-sigstore-help@/{f=1;next} /@end-sigstore-help@/{f=0} f' \ < README.md | sed '1d;$$d' \ ) \ <( \ $(MAKE) -s run ARGS="--help" \ ) # sigstore sign --help @diff \ <( \ awk '/@begin-sigstore-sign-help@/{f=1;next} /@end-sigstore-sign-help@/{f=0} f' \ < README.md | sed '1d;$$d' \ ) \ <( \ $(MAKE) -s run ARGS="sign --help" \ ) # sigstore attest --help @diff \ <( \ awk '/@begin-sigstore-attest-help@/{f=1;next} /@end-sigstore-attest-help@/{f=0} f' \ < README.md | sed '1d;$$d' \ ) \ <( \ $(MAKE) -s run ARGS="attest --help" \ ) # sigstore verify identity --help @diff \ <( \ awk '/@begin-sigstore-verify-identity-help@/{f=1;next} /@end-sigstore-verify-identity-help@/{f=0} f' \ < README.md | sed '1d;$$d' \ ) \ <( \ $(MAKE) -s run ARGS="verify identity --help" \ ) # sigstore verify github --help @diff \ <( \ awk '/@begin-sigstore-verify-github-help@/{f=1;next} /@end-sigstore-verify-github-help@/{f=0} f' \ < README.md | sed '1d;$$d' \ ) \ <( \ $(MAKE) -s run ARGS="verify github --help" \ ) .PHONY: edit edit: $(EDITOR) $(ALL_PY_SRCS) update-embedded-root: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ python -m sigstore plumbing update-trust-root cp ~/.local/share/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json \ sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json cp ~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json \ ~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/signing_config.v0.2.json \ sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/ update-embedded-root-staging: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ python -m sigstore --staging plumbing update-trust-root cp ~/.local/share/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/root.json \ sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/root.json cp ~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/trusted_root.json \ ~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/signing_config.v0.2.json \ sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/ ================================================ FILE: README.md ================================================ sigstore-python =============== [![CI](https://github.com/sigstore/sigstore-python/workflows/CI/badge.svg)](https://github.com/sigstore/sigstore-python/actions/workflows/ci.yml) [![PyPI version](https://badge.fury.io/py/sigstore.svg)](https://pypi.org/project/sigstore) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/sigstore/sigstore-python/badge)](https://securityscorecards.dev/viewer/?uri=github.com/sigstore/sigstore-python) [![SLSA](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev/) ![Conformance Tests](https://github.com/sigstore/sigstore-python/workflows/Conformance%20Tests/badge.svg) [![Documentation](https://github.com/sigstore/sigstore-python/actions/workflows/docs.yml/badge.svg)](https://sigstore.github.io/sigstore-python) `sigstore` is a Python tool for generating and verifying Sigstore signatures. You can use it to sign and verify Python package distributions, or anything else! ## Index * [Features](#features) * [Installation](#installation) * [Usage](#usage) * [Signing](#signing) * [Verifying](#verifying) * [Generic identities](#generic-identities) * [Signatures from GitHub Actions](#signatures-from-github-actions) * [Advanced usage](#advanced-usage) * [Troubleshooting](#troubleshooting) * [Documentation](#documentation) * [Licensing](#licensing) * [Community](#community) * [Contributing](#contributing) * [Code of Conduct](#code-of-conduct) * [Security](#security) * [SLSA Provenance](#slsa-provenance) ## Features * Support for keyless signature generation and verification with [Sigstore](https://www.sigstore.dev/) * Support for signing with ["ambient" OpenID Connect identities](https://github.com/sigstore/sigstore-python#signing-with-ambient-credentials) * A comprehensive [CLI](https://github.com/sigstore/sigstore-python#usage) and corresponding [importable Python API](https://sigstore.github.io/sigstore-python) ## Installation `sigstore` requires Python 3.10 or newer, and can be installed directly via `pip`: ```console python -m pip install sigstore ``` See the [installation](https://sigstore.github.io/sigstore-python/installation) page in the documentation for more installation options. ## Usage For Python API usage, see our [API](https://sigstore.github.io/sigstore-python/api/). You can run `sigstore` as a standalone program: ```console sigstore --help ``` Top-level: ``` usage: sigstore [-h] [-v] [-V] [--staging | --instance URL | --trust-config FILE] COMMAND ... a tool for signing and verifying Python package distributions positional arguments: COMMAND the operation to perform attest sign one or more inputs using DSSE sign sign one or more inputs verify verify one or more inputs get-identity-token retrieve and return a Sigstore-compatible OpenID Connect token trust-instance Initialize trust for a Sigstore instance plumbing developer-only plumbing operations options: -h, --help show this help message and exit -v, --verbose run with additional debug logging; supply multiple times to increase verbosity (default: 0) -V, --version show program's version number and exit --staging Use sigstore's staging instance, instead of the default production instance. Mutually exclusive with other instance configuration arguments. (default: False) --instance URL Use a given Sigstore instance URL, instead of the default production instance. Mutually exclusive with other instance configuration arguments. (default: None) --trust-config FILE Use given client trust configuration instead of using the default production instance. Mutually exclusive with other instance configuration arguments. (default: None) ``` ### Signing ``` usage: sigstore sign [-h] [-v] [--rekor-version VERSION] [--identity-token TOKEN] [--oidc-client-id ID] [--oidc-client-secret SECRET] [--oidc-disable-ambient-providers] [--oidc-issuer URL] [--oauth-force-oob] [--no-default-files] [--signature FILE] [--certificate FILE] [--bundle FILE] [--output-directory DIR] [--overwrite] FILE [FILE ...] positional arguments: FILE The file to sign options: -h, --help show this help message and exit -v, --verbose run with additional debug logging; supply multiple times to increase verbosity (default: 0) --rekor-version VERSION Force the rekor transparency log version. Valid values are [1, 2]. By default the highest available version is used OpenID Connect options: --identity-token TOKEN the OIDC identity token to use (default: None) --oidc-client-id ID The custom OpenID Connect client ID to use during OAuth2 (default: sigstore) --oidc-client-secret SECRET The custom OpenID Connect client secret to use during OAuth2 (default: None) --oidc-disable-ambient-providers Disable ambient OpenID Connect credential detection (e.g. on GitHub Actions) (default: False) --oidc-issuer URL The OpenID Connect issuer to use (default: None) --oauth-force-oob Force an out-of-band OAuth flow and do not automatically start the default web browser (default: False) Output options: --no-default-files Don't emit the default output files ({input}.sigstore.json) (default: False) --signature FILE, --output-signature FILE Write a single signature to the given file; does not work with multiple input files (default: None) --certificate FILE, --output-certificate FILE Write a single certificate to the given file; does not work with multiple input files (default: None) --bundle FILE Write a single Sigstore bundle to the given file; does not work with multiple input files (default: None) --output-directory DIR Write default outputs to the given directory (conflicts with --signature, --certificate, --bundle) (default: None) --overwrite Overwrite preexisting signature and certificate outputs, if present (default: False) ``` ### Signing with DSSE envelopes ``` usage: sigstore attest [-h] [-v] [--rekor-version VERSION] --predicate FILE --predicate-type TYPE [--identity-token TOKEN] [--oidc-client-id ID] [--oidc-client-secret SECRET] [--oidc-disable-ambient-providers] [--oidc-issuer URL] [--oauth-force-oob] [--bundle FILE] [--overwrite] FILE [FILE ...] positional arguments: FILE The file to sign options: -h, --help show this help message and exit -v, --verbose run with additional debug logging; supply multiple times to increase verbosity (default: 0) --rekor-version VERSION Force the rekor transparency log version. Valid values are [1, 2]. By default the highest available version is used DSSE options: --predicate FILE Path to the predicate file (default: None) --predicate-type TYPE Specify a predicate type (https://slsa.dev/provenance/v0.2, https://slsa.dev/provenance/v1) (default: None) OpenID Connect options: --identity-token TOKEN the OIDC identity token to use (default: None) --oidc-client-id ID The custom OpenID Connect client ID to use during OAuth2 (default: sigstore) --oidc-client-secret SECRET The custom OpenID Connect client secret to use during OAuth2 (default: None) --oidc-disable-ambient-providers Disable ambient OpenID Connect credential detection (e.g. on GitHub Actions) (default: False) --oidc-issuer URL The OpenID Connect issuer to use (default: None) --oauth-force-oob Force an out-of-band OAuth flow and do not automatically start the default web browser (default: False) Output options: --bundle FILE Write a single Sigstore bundle to the given file; does not work with multiple input files (default: None) --overwrite Overwrite preexisting bundle outputs, if present (default: False) ``` ### Verifying #### Identities ``` usage: sigstore verify identity [-h] [-v] [--certificate FILE] [--signature FILE] [--bundle FILE] [--offline] --cert-identity IDENTITY --cert-oidc-issuer URL FILE_OR_DIGEST [FILE_OR_DIGEST ...] options: -h, --help show this help message and exit -v, --verbose run with additional debug logging; supply multiple times to increase verbosity (default: 0) Verification inputs: --certificate FILE, --cert FILE The PEM-encoded certificate to verify against; not used with multiple inputs (default: None) --signature FILE The signature to verify against; not used with multiple inputs (default: None) --bundle FILE The Sigstore bundle to verify with; not used with multiple inputs (default: None) FILE_OR_DIGEST The file path or the digest to verify. The digest should start with the 'sha256:' prefix. Verification options: --offline Perform offline verification; requires a Sigstore bundle (default: False) --cert-identity IDENTITY The identity to check for in the certificate's Subject Alternative Name (default: None) --cert-oidc-issuer URL The OIDC issuer URL to check for in the certificate's OIDC issuer extension (default: None) ``` #### Signatures from GitHub Actions ``` usage: sigstore verify github [-h] [-v] [--certificate FILE] [--signature FILE] [--bundle FILE] [--offline] [--cert-identity IDENTITY] [--trigger EVENT] [--sha SHA] [--name NAME] [--repository REPO] [--ref REF] FILE_OR_DIGEST [FILE_OR_DIGEST ...] options: -h, --help show this help message and exit -v, --verbose run with additional debug logging; supply multiple times to increase verbosity (default: 0) Verification inputs: --certificate FILE, --cert FILE The PEM-encoded certificate to verify against; not used with multiple inputs (default: None) --signature FILE The signature to verify against; not used with multiple inputs (default: None) --bundle FILE The Sigstore bundle to verify with; not used with multiple inputs (default: None) FILE_OR_DIGEST The file path or the digest to verify. The digest should start with the 'sha256:' prefix. Verification options: --offline Perform offline verification; requires a Sigstore bundle (default: False) --cert-identity IDENTITY The identity to check for in the certificate's Subject Alternative Name (default: None) --trigger EVENT The GitHub Actions event name that triggered the workflow (default: None) --sha SHA The `git` commit SHA that the workflow run was invoked with (default: None) --name NAME The name of the workflow that was triggered (default: None) --repository REPO The repository slug that the workflow was triggered under (default: None) --ref REF The `git` ref that the workflow was invoked with (default: None) ``` ## Troubleshooting First, please make sure you are using a recent and supported release: sigstore-python project provides support for the latest release and best effort critical bug fixes for the latest 3.6.x release. ### Common issues 1. "_bundle contains a transparency log entry that is incompatible with this version of sigstore-python_" (as well as "_not enough sources of verified time_") means an upgrade is necessary to verify this signature bundle: Signature bundles with Rekor v2 transparency log entries can only be verified with sigstore-python 4 and above 1. verifying without a network connection results in HTTP errors: By default sigstore-python checks for updates to the trusted key material on every startup. This can be avoided temporarily with `--offline` but please read the [documentation](https://sigstore.github.io/sigstore-python/advanced/offline/) for caveats 1. Signing results in HTTP errors: Signing with sigstore-python depends on multiple Sigstore services. Retrying on failure may be a useful workaround if any of these services fail but filing issues for specific failures is appreciated ### My problem is something else Please [open an issue](https://github.com/sigstore/sigstore-python/issues/new?template=bug.md) or ask in the [slack channel](#community). ## Documentation `sigstore` documentation is available on [https://sigstore.github.io/sigstore-python](https://sigstore.github.io/sigstore-python) ## Licensing `sigstore` is licensed under the Apache 2.0 License. ## Community `sigstore-python` is developed as part of the [Sigstore](https://sigstore.dev) project. We also use a [Slack channel](https://sigstore.slack.com)! Click [here](https://join.slack.com/t/sigstore/shared_invite/zt-mhs55zh0-XmY3bcfWn4XEyMqUUutbUQ) for the invite link. ## Contributing See [the contributing docs](https://github.com/sigstore/.github/blob/main/CONTRIBUTING.md) for details. ## Code of Conduct Everyone interacting with this project is expected to follow the [sigstore Code of Conduct](https://github.com/sigstore/.github/blob/main/CODE_OF_CONDUCT.md). ## Security Should you discover any security issues, please refer to sigstore's [security process](https://github.com/sigstore/.github/blob/main/SECURITY.md). ================================================ FILE: cloudbuild.yaml ================================================ steps: # Install dependencies - name: python entrypoint: python args: ["-m", "pip", "install", ".", "--user"] # Sign with ambient GCP credentials - name: python entrypoint: python args: ["-m", "sigstore", "sign", "README.md"] env: - "GOOGLE_SERVICE_ACCOUNT_NAME=sigstore-python-test@projectsigstore.iam.gserviceaccount.com" ================================================ FILE: docs/advanced/custom_trust.md ================================================ # Custom Sigstore instances By default, `sigstore` is configured to work with the public `sigstore.dev` instance. The trust materials for this instance are bundled with the client, allowing for a seamless out-of-the-box experience. In addition to the public instance, `sigstore` also supports using custom Sigstore instances. When using a custom instance, you are responsible for providing the trust materials (at least once). This document outlines the methods for doing so. ### Using a custom instance Using a custom Sigstore instance is a two-step process: 1. First, you must establish trust for the new instance. This is done using the `sigstore trust-instance` command. This step only needs to be performed once. 2. Once trust is established, you can use the `--instance` flag with `sigstore` commands like `sign` and `verify` to point to your custom instance. To establish trust for a custom instance, you need its TUF root file. You can then run: ```console $ sigstore --instance https://my-sigstore.example.com trust-instance my-root.json ``` After successfully adding the new instance, you can use it for signing and verifying artifacts. For example, to sign a file: ```console $ sigstore --instance https://my-sigstore.example.com sign foo.txt ``` ### Using a custom instance with local configuration The trust configuration can also be provided as a local file -- but the user is now responsible for keeping the trust configuration updated. The `--trust-config` flag, accepts a JSON-formatted file conforming to the `ClientTrustConfig` message in the [Sigstore protobuf specs](https://github.com/sigstore/protobuf-specs). This file configures the entire Sigstore instance state, *including* the URIs used to access the CA and artifact transparency services as well as the cryptographic root of trust itself. To use a custom client config, prepend `--trust-config` to any `sigstore` command: ```console $ sigstore --trust-config custom.trustconfig.json sign foo.txt $ sigstore --trust-config custom.trustconfig.json verify identity foo.txt ... ``` ================================================ FILE: docs/advanced/offline.md ================================================ # Offline Verification !!! danger Because `--offline` disables trust root updates, `sigstore-python` falls back to the latest cached trust root or, if none exists, the trust root baked into `sigstore-python` itself. Like with any other offline verification, this means that users may miss trust root changes (such as new root keys, or revocations) unless they separately keep the trust root up-to-date. Users who need to operationalize offline verification may wish to do this by distributing their own trust configuration; see [Custom instance with local configuration](./custom_trust.md#using-a-custom-instance-with-local-configuration). During verification, there are two kinds of network access that `sigstore-python` *can* perform: 1. When verifying against "detached" materials (e.g. separate `.crt` and `.sig` files), `sigstore-python` can perform an online transparency log lookup. 2. By default, during all verifications, `sigstore-python` will attempt to refresh the locally cached root of trust via a TUF update. When performing bundle verification (i.e. `.sigstore` or `.sigstore.json`), (1) does not apply. However, (2) can still result in online accesses. To perform **fully** offline verification, pass `--offline` to your `sigstore verify` subcommand: ```bash $ sigstore verify identity foo.txt \ --offline \ --cert-identity 'hamilcar@example.com' \ --cert-oidc-issuer 'https://github.com/login/oauth' ``` Alternatively, users may choose to bypass TUF entirely by passing an entire trust configuration to `sigstore-python` via `--trust-config`: ```bash $ sigstore --trust-config public.trustconfig.json verify identity ... ``` This will similarly result in fully offline operation, as the trust configuration contains a full trust root. ================================================ FILE: docs/api/errors.md ================================================ :::sigstore.errors ================================================ FILE: docs/api/hashes.md ================================================ :::sigstore.hashes ================================================ FILE: docs/api/index.md ================================================ !!! note The API reference is automatically generated from the docstrings :::sigstore ================================================ FILE: docs/api/models.md ================================================ :::sigstore.models ================================================ FILE: docs/api/oidc.md ================================================ :::sigstore.oidc ================================================ FILE: docs/api/sign.md ================================================ :::sigstore.sign ================================================ FILE: docs/api/verify/policy.md ================================================ :::sigstore.verify.policy ================================================ FILE: docs/api/verify/verifier.md ================================================ :::sigstore.verify.verifier ================================================ FILE: docs/index.md ================================================ # Home ## Introduction `sigstore` is a Python tool for generating and verifying [Sigstore] signatures. You can use it to sign and verify Python package distributions, or anything else! ## Features * Support for keyless signature generation and verification with [Sigstore](https://www.sigstore.dev/) * Support for signing with ["ambient" OpenID Connect identities](./signing.md#signing-with-ambient-credentials) * A comprehensive [CLI](#using-sigstore) and corresponding [importable Python API](./api/index.md) ## Installing `sigstore` ```console python -m pip install sigstore ``` See [installation](./installation.md) for more detailed installation instructions or options. ## Using `sigstore` You can run `sigstore` as a standalone program, or via `python -m`: ```console sigstore --help python -m sigstore --help ``` - Use `sigstore` to [sign](./signing.md) - Use `sigstore` to [verify](./verify.md) ## SLSA Provenance This project emits a [SLSA] provenance on its release! This enables you to verify the integrity of the downloaded artifacts and ensured that the binary's code really comes from this source code. To do so, please follow the instructions [here](https://github.com/slsa-framework/slsa-github-generator#verification-of-provenance). [SLSA]: https://slsa.dev/ [Sigstore]: https://www.sigstore.dev/ ================================================ FILE: docs/installation.md ================================================ # Installation ## With `pip` `sigstore` requires Python 3.9 or newer, and can be installed directly via `pip`: ```console python -m pip install sigstore ``` Optionally, to install `sigstore` and all its dependencies with [hash-checking mode](https://pip.pypa.io/en/stable/topics/secure-installs/#hash-checking-mode) enabled, run the following: ```console python -m pip install -r https://raw.githubusercontent.com/sigstore/sigstore-python/main/install/requirements.txt ``` This installs the requirements file located [here](https://github.com/sigstore/sigstore-python/blob/main/install/requirements.txt), which is kept up-to-date. ## With `uv` !!! warning `sigstore` depends on `betterproto` pre-releases versions, which are by default not resolved by `uv`. ```console uv pip install --prerelease=allow sigstore ``` `sigstore` can also be used as tool: ```console uvx --prerelease=allow sigstore --help ``` ## GitHub Actions `sigstore-python` has [an official GitHub Action](https://github.com/sigstore/gh-action-sigstore-python)! You can install it from the [GitHub Marketplace](https://github.com/marketplace/actions/gh-action-sigstore-python), or add it to your CI manually: ```yaml jobs: sigstore-python: steps: - uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: foo.txt ``` See the [action documentation](https://github.com/sigstore/gh-action-sigstore-python/blob/main/README.md) for more details and usage examples. ================================================ FILE: docs/policy.md ================================================ # Policies This document describes the set of policies followed by `sigstore-python` when signing or verifying a bundle. `sigstore-python` follows the [Sigstore: Client Spec] and this document outline mimic the one from the spec. ## Signing ### Authentication `sigstore-python` supports several authentication mechanisms : - An OAuth flow: this mode is preferred for interactive workflows. - An _ambient_ detection: this mode is preferred for un-attended workflows (i.e., continuous integration system) ### Key generation `sigstore-python` uses [ECDSA] as its signing algorithm. ### Certificate Issuance _using Fulcio_ ### Signing When needed, the payload pre-hashing algorithm is `SHA2_256`. ### Timestamping If Timestamp Authorities have been provided in the Signing Config, a Timestamp Request using the hash of the signature is automatically sent to the provided Timestamp Authorities. This step allows to attest of the signature time. ### Submission of Signing Metadata to Transparency Service The Transparency Service, [rekor], is used by `sigstore-python` to provide a public, immutable record of signing events. This step is crucial for ensuring the integrity and transparency of the signing process. !!! warning This step is performed before the `Timestamping` step in the workflow. ### Signing Choices Here's a summary of the key choices in the `sigstore-python` signing process: | Option | `sigstore-python` | |-------------------------------|------------------------------| | Digital signature algorithm | ECDSA | | Signature metadata format | ??? | | Payload pre-hashing algorithm | SHA2 (256) | | Long-lived signing keys | not used | | Timestamping | Used if provided | | Transparency | Always used (rekor) | | Other workflows | no other workflows supported | ## Verification `sigstore-python` supports configuring the verification process using policies but this must be done using the [api](./api/index.md). By default, the CLI uses the [`Identity`][sigstore.verify.policy] verification policy. ### Establishing a Time for the Signature If the bundle contains one or more signed times from Timestamping Authorities, they will be used as the time source. In this case, a Timestamp Authority configuration must be provided in the `ClientTrustConfig`. When verifying Timestamp Authorities Responses, at least one must be valid. If there is a Transparency Service Timestamp, this is also used as a source of trusted time. The verification will fail if no sources of time are found. ### Certificate For a signature to be considered valid, it must meet two key criteria: - The signature must have an associated timestamp. - Every certificate in the chain, from the signing certificate up to the root certificate, must be valid at the time of signing. This approach is known as the “hybrid model” of certificate verification, as described by [Braun et al.]. This validation process is repeated for each available source of trusted time. The signature is only considered valid if it passes the validation checks against all of these time sources. #### SignedCertificateTimestamp The `SignedCertificateTimestamp` is extracted from the leaf certificate and verified using the verification key from the Certificate Transparency Log. #### Identity Verification Policy The system verifies that the signing certificate conforms to the Sigstore X. 509 profile as well as `Identity Policy`. ### Transparency Log Entry The Verifier now verifies the inclusion proof and signed checkpoint for the log entry using [rekor]. If there is an inclusion promise, this is also verified. #### Time insertion check The system verifies that the transparency log entry’s insertion timestamp falls within the certificate’s validity period. If the insertion timestamp is outside the certificate’s validity period, it could indicate potential backdating or use of an expired certificate, and the verification will fail. ### Signature Verification The next verification step is to verify the actual signature. This ensures that the signed content has not been tampered with and was indeed signed by the claimed entity. The verification process differs slightly depending on the type of signed content: - DSSE: The entire envelope structure is used as the verification payload. - Artifacts: The raw bytes of the artifacts serve as the verification payload. #### Final step Finally, a last consistency check is performed to verify that the constructed payload is indeed the one that has been signed. This step is ussed to prevent variants of [CVE-2022-36056]. [Sigstore: Client Spec]: https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit?usp=sharing [ECDSA]: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm [rekor]: https://github.com/sigstore/rekor [Braun et al.]: https://research.tue.nl/en/publications/how-to-avoid-the-breakdown-of-public-key-infrastructures-forward- [CVE-2022-36056]: https://github.com/sigstore/cosign/security/advisories/GHSA-8gw7-4j42-w388 ================================================ FILE: docs/scripts/gen_ref_pages.py ================================================ # 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. import argparse import shutil import sys from pathlib import Path root = Path(__file__).parent.parent.parent src = root / "sigstore" api_root = root / "docs" / "api" def main(args: argparse.Namespace) -> None: """Main script.""" if args.overwrite: shutil.rmtree(api_root, ignore_errors=True) elif not args.check and api_root.exists(): print(f"API root {api_root} already exists, skipping.") sys.exit(0) seen = set() for path in src.rglob("*.py"): module_path = path.relative_to(src).with_suffix("") full_doc_path = api_root / path.relative_to(src).with_suffix(".md") # Exclude private entries if any(part.startswith("_") for part in module_path.parts): continue if args.check and not full_doc_path.is_file(): print(f"File {full_doc_path} does not exist.", file=sys.stderr) sys.exit(1) full_doc_path.parent.mkdir(parents=True, exist_ok=True) with full_doc_path.open("w") as f: f.write(f":::sigstore.{str(module_path).replace('/', '.')}\n ") seen.add(full_doc_path) # Add the root with (api_root / "index.md").open("w") as f: f.write("""!!! note The API reference is automatically generated from the docstrings :::sigstore """) seen.add(api_root / "index.md") if args.check: if diff := set(api_root.rglob("*.md")).symmetric_difference(seen): print(f"Found leftover documentation file: {diff}", file=sys.stderr) sys.exit(1) else: print("API doc generated.") if __name__ == "__main__": parser = argparse.ArgumentParser( description="Generate the structure for the API documentation." ) parser.add_argument("--overwrite", action="store_true", default=False) parser.add_argument("--check", action="store_true", default=False) arguments = parser.parse_args() if arguments.check and arguments.overwrite: print("You can't specify both --check and --overwrite.", file=sys.stderr) sys.exit(1) main(arguments) ================================================ FILE: docs/signing.md ================================================ # Signing !!! warning By default signing an artifact creates a public record in `Rekor` which is publicly available. The transparency log entry is browsable at `https://search.sigstore.dev/?logIndex=` and disclose the signing identity. ## Identities ### Signing with ambient credentials For environments that support OpenID Connect, `sigstore` supports ambient credential detection. This includes many popular CI platforms and cloud providers. See the full list of supported environments [here](https://github.com/di/id#supported-environments). ### Signing with an email identity `sigstore` can use an OAuth2 + OpenID flow to establish an email identity, allowing you to request signing certificates that attest to control over that email. By default, `sigstore` attempts to do [ambient credential detection](#signing-with-ambient-credentials), which may preempt the OAuth2 flow. To force the OAuth2 flow, you can explicitly disable ambient detection: ```console $ sigstore sign --oidc-disable-ambient-providers foo.txt ``` ### Signing with an explicit identity token If you can't use an ambient credential or the OAuth2 flow, you can pass a pre-created identity token directly into `sigstore sign`: ```console $ sigstore sign --identity-token YOUR-LONG-JWT-HERE foo.txt ``` Note that passing a custom identity token does not circumvent Fulcio's requirements, namely the Fulcio's supported identity providers and the claims expected within the token. !!! note The examples in the section below are using ambient credential detection. When no credentials are detected, it opens a browser to perform an interactive OAuth2 authentication flow. ## Signing an artifact The easiest option to sign an artifact with `sigstore` is to use the `sign` command. For example, signing `sigstore-python` [README.md](https://github.com/sigstore/sigstore-python/blob/main/README.md). ```console $ sigstore sign README.md Waiting for browser interaction... Using ephemeral certificate: -----BEGIN CERTIFICATE----- MIIC2TCCAl+gAwIBAgIUdqkRnuxTr6bgdKtNiItu3+y8UkIwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjQxMjEyMDk1NTU5WhcNMjQxMjEyMTAwNTU5WjAAMFkwEwYH KoZIzj0CAQYIKoZIzj0DAQcDQgAEjb33vsuuNr4phkmpkUvMB19rnXLtS9QqZGT+ kDetyi9+wYv/g2oOFDfEm7UHPLUeZJ6Bad8Zd7H/JqGUhuJ7gaOCAX4wggF6MA4G A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUJpNq 0mPqLw1ypudG98REMY7mjyowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y ZD8wLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5j b20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsG CisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgor BgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p 7o4AAAGTukvv5QAABAMARzBFAiEA3oqdIinnZ9rGb7CTxQ60G6xi6l3T+z6vkSr2 ERAnIp4CIHbx61camOWU8dClH2WMUfguQ11+D82IQQBnHF968g22MAoGCCqGSM49 BAMDA2gAMGUCMQDdf8S5Y/UhAp2vd2eo+RsjtfsasXSI51kO1ppNz42rSa6b5djW 8+we6/OzVQW+THYCMBaBHPNntloKD040Pce6f8W3HpydbUzshJ24Emt/EaTPqH/g gYd2xz5hd4vQ7Ysmsg== -----END CERTIFICATE----- Transparency log entry created at index: 155016378 MEQCIHVjH0I3iarhB5hD0MEE4AZ7GpCPZhXpdsVsSFlZIynVAiA10qzWt9FBC5pjD6+1kLRS14F+muVD1NJZNw6b+/WADQ== Sigstore bundle written to README.md.sigstore.json ``` The log entry is available at : [https://search.sigstore.dev/?logIndex=155016378](https://search.sigstore.dev/?logIndex=155016378) ## Attest `sigstore` can be used to generate attestations for software artifacts using [SLSA]. !!! info "What is SLSA?" Supply-chain Levels for Software Artifacts, or SLSA ("salsa"). It’s a security framework, a checklist of standards and controls to prevent tampering, improve integrity, and secure packages and infrastructure. It’s how you get from "safe enough" to being as resilient as possible, at any link in the chain. At the moment, `sigstore` supports the following predicates types: - [https://slsa.dev/provenance/v1](https://slsa.dev/spec/v1.0/provenance) - [https://slsa.dev/provenance/v0.2](https://slsa.dev/spec/v0.2/provenance) Example : ```console $ sigstore attest \ --predicate-type "https://slsa.dev/provenance/v1" \ --predicate ./test/assets/integration/attest/slsa_predicate_v1_0.json \ ./README.md Waiting for browser interaction... Using ephemeral certificate: -----BEGIN CERTIFICATE----- MIIC2TCCAmCgAwIBAgIUI1GUnwGV69rXWAixrFmwAcZ7j7IwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjQxMjEyMTAxODUwWhcNMjQxMjEyMTAyODUwWjAAMFkwEwYH KoZIzj0CAQYIKoZIzj0DAQcDQgAEZPieQV37ByUyf+zWMGjXmom+kM4INxPcO1Kf DhjV3RmhTAlKOYXGU38O/KUNka5BLTb4f5r1bNwGhiEf9qcmNqOCAX8wggF7MA4G A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUUexC qnLoKejMCAAgNxN77wSlIHkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y ZD8wLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5j b20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsG CisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGLBgor BgEEAdZ5AgQCBH0EewB5AHcA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p 7o4AAAGTumDcJAAABAMASDBGAiEAprGPiBTcRK8ZFM+x3HLE+2s82xPAecHfJo9F RXNI+CMCIQCYzRBQtTehd+LLmwkXjPJEsJ5CpI7q1uDhhspyplVSLjAKBggqhkjO PQQDAwNnADBkAjAjO7BG9Gx6ggm1/IP75l+LzUnAP/DP0BOBeM0/lXZN3BBUvtdq +oTUzmmY/VpCWggCMEcCMn4UDIF/jBrVhES8ks57T8LjRX6xacpn9ufpkTlnKs6w S8/kL6jEREOcdnpOSQ== -----END CERTIFICATE----- Transparency log entry created at index: 155019253 Sigstore bundle written to README.md.sigstore.json ``` [SLSA]: https://slsa.dev/ ================================================ FILE: docs/stylesheets/custom.css ================================================ /* From https://github.com/sigstore/community/blob/main/artwork/Sigstore_BrandGuide_March2023.pdf */ :root { --md-primary-fg-color: #2e2f71; --md-primary-bg-color: #f9f7ef; } ================================================ FILE: docs/verify.md ================================================ # Verifying ## Generic identities This is the most common verification done with `sigstore`, and therefore the one you probably want: you can use it to verify that a signature was produced by a particular identity (like `hamilcar@example.com`), as attested to by a particular OIDC provider (like `https://github.com/login/oauth`). ```console $ sigstore verify identity --cert-identity --cert-oidc-issuer FILE_OR_DIGEST ``` The following command will verify that the bundle `tests/assets/bundle.txt.sigstore` was signed by `a@tny.town` using the staging infrastructure of `sigstore`. ```console $ sigstore --staging verify identity --cert-identity "a@tny.town" --cert-oidc-issuer "https://github.com/login/oauth" test/assets/bundle.txt ``` ## Verifying from GitHub Actions If your signatures are coming from GitHub Actions (e.g., a workflow that uses its [ambient credentials](./signing.md#signing-with-ambient-credentials)), then you can use the `sigstore verify github` subcommand to verify claims more precisely than `sigstore verify identity` allows. `sigstore verify github` can be used to verify claims specific to signatures coming from GitHub Actions. `sigstore-python` signs releases via GitHub Actions, so the examples below are working examples of how you can verify a given `sigstore-python` release. When using `sigstore verify github`, you must pass `--cert-identity` or `--repository`, or both. Unlike `sigstore verify identity`, `--cert-oidc-issuer` is **not** required (since it's inferred to be GitHub Actions). Verifying with `--cert-identity`: ```console $ sigstore verify github sigstore-0.10.0-py3-none-any.whl \ --bundle sigstore-0.10.0-py3-none-any.whl.bundle \ --cert-identity https://github.com/sigstore/sigstore-python/.github/workflows/release.yml@refs/tags/v0.10.0 ``` Verifying with `--repository`: ```console $ sigstore verify github sigstore-0.10.0-py3-none-any.whl \ --bundle sigstore-0.10.0-py3-none-any.whl.bundle \ --repository sigstore/sigstore-python ``` Additional GitHub Actions specific claims can be verified like so: ```console $ sigstore verify github sigstore-0.10.0-py3-none-any.whl \ --bundle sigstore-0.10.0-py3-none-any.whl.bundle \ --cert-identity https://github.com/sigstore/sigstore-python/.github/workflows/release.yml@refs/tags/v0.10.0 \ --trigger release \ --sha 66581529803929c3ccc45334632ccd90f06e0de4 \ --name Release \ --repository sigstore/sigstore-python \ --ref refs/tags/v0.10.0 ``` ## Verifying against a bundle By default, `sigstore verify identity` will attempt to find a `.sigstore.json` or `.sigstore` in the same directory as the file being verified: ```console # looks for foo.txt.sigstore.json $ sigstore verify identity foo.txt \ --cert-identity 'hamilcar@example.com' \ --cert-oidc-issuer 'https://github.com/login/oauth' ``` Multiple files can be verified at once: ```console # looks for {foo,bar}.txt.sigstore.json $ python -m sigstore verify identity foo.txt bar.txt \ --cert-identity 'hamilcar@example.com' \ --cert-oidc-issuer 'https://github.com/login/oauth' ``` ## Verifying a digest instead of a file `sigstore-python` supports verifying digests directly, without requiring the artifact to be present. The digest should be prefixed with the `sha256:` string: ```console $ sigstore verify identity sha256:ce8ab2822671752e201ea1e19e8c85e73d497e1c315bfd9c25f380b7625d1691 \ --cert-identity 'hamilcar@example.com' \ --cert-oidc-issuer 'https://github.com/login/oauth' --bundle 'foo.txt.sigstore.json' ``` ================================================ FILE: install/requirements.in ================================================ sigstore==4.2.0 ================================================ FILE: mkdocs.yml ================================================ # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json site_name: sigstore-python site_url: https://sigstore.github.io/sigstore-python repo_url: https://github.com/sigstore/sigstore-python site_description: sigstore-python, a Sigstore client written in Python repo_name: sigstore-python edit_uri: edit/main/docs/ theme: name: material icon: repo: fontawesome/brands/github logo: assets/images/logo.png features: - content.action.edit - content.code.copy - header.autohide - navigation.instant - navigation.instant.progress - navigation.footer - search.highlight - search.suggest palette: primary: custom font: text: Inter extra_css: - stylesheets/custom.css nav: - Home: index.md - Installation: installation.md - Signing: signing.md - Verifying: verify.md - Policy: policy.md - Advanced: - Custom Sigstore Instances: advanced/custom_trust.md - Offline Verification: advanced/offline.md # begin-api-section - API: - api/index.md - Models: api/models.md - Errors: api/errors.md - Hashes: api/hashes.md - OIDC: api/oidc.md - Sign: api/sign.md - Verify: - Policy: api/verify/policy.md - Verifier: api/verify/verifier.md # end-api-section markdown_extensions: - admonition - pymdownx.details - pymdownx.superfences copyright: sigstore © 2024 plugins: - search - social - mkdocstrings: handlers: python: options: members_order: source unwrap_annotated: true modernize_annotations: true merge_init_into_class: true docstring_section_style: spacy signature_crossrefs: true show_symbol_type_toc: true filters: - '!^_' validation: omitted_files: warn unrecognized_links: warn anchors: warn not_found: warn extra: generator: false social: - icon: fontawesome/brands/slack link: https://sigstore.slack.com - icon: fontawesome/brands/x-twitter link: https://twitter.com/projectsigstore ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "sigstore" dynamic = ["version"] description = "A tool for signing Python package distributions" readme = "README.md" license = { file = "LICENSE" } authors = [ { name = "Sigstore Authors", email = "sigstore-dev@googlegroups.com" }, ] classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Security", "Topic :: Security :: Cryptography", ] dependencies = [ "cryptography >= 42, < 47", "id >= 1.1.0", "importlib_resources ~= 5.7; python_version < '3.11'", "pyasn1 ~= 0.6", "pydantic >= 2,< 3", "pyjwt >= 2.1", "pyOpenSSL >= 23.0.0", "requests", "rich >= 13,< 16", "rfc8785 ~= 0.1.2", "rfc3161-client >= 1.0.3,< 1.1.0", # Both sigstore-models and sigstore-rekor types are unstable # so we pin them conservatively. "sigstore-models == 0.0.6", "sigstore-rekor-types == 0.0.18", "tuf ~= 6.0", "platformdirs ~= 4.2", ] requires-python = ">=3.10" [project.scripts] sigstore = "sigstore._cli:main" [project.urls] Homepage = "https://pypi.org/project/sigstore/" Issues = "https://github.com/sigstore/sigstore-python/issues" Source = "https://github.com/sigstore/sigstore-python" Documentation = "https://sigstore.github.io/sigstore-python/" [project.optional-dependencies] test = ["pytest", "pytest-cov", "pretend", "coverage[toml]"] lint = [ "bandit", # "interrogate >= 1.7.0", "mypy ~= 1.1", # NOTE(ww): ruff is under active development, so we pin conservatively here # and let Dependabot periodically perform this update. "ruff < 0.15.12", "types-requests", "types-pyOpenSSL", ] doc = ["mkdocs-material[imaging]", "mkdocstrings-python"] dev = ["build", "bump >= 1.3.2", "sigstore[doc,test,lint]"] [tool.coverage.run] # branch coverage in addition to statement coverage. branch = true # FIXME(jl): currently overridden. see: https://pytest-cov.readthedocs.io/en/latest/config.html # include machine name, process id, and a random number in `.coverage-*` so each file is distinct. parallel = true # store relative path info for aggregation across runs with potentially differing filesystem layouts. # see: https://coverage.readthedocs.io/en/7.1.0/config.html#config-run-relative-files relative_files = true # don't attempt code coverage for the CLI entrypoints omit = ["sigstore/_cli.py"] [tool.coverage.report] exclude_lines = [ "@abc.abstractmethod", "@typing.overload", "if typing.TYPE_CHECKING", ] [tool.interrogate] # don't enforce documentation coverage for packaging, testing, the virtual # environment, or the CLI (which is documented separately). exclude = ["env", "test", "sigstore/_cli.py"] ignore-semiprivate = true ignore-private = true # Ignore nested classes for docstring coverage because we use them primarily # for pydantic model configuration. ignore-nested-classes = true fail-under = 100 [tool.mypy] allow_redefinition = true check_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_defs = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] ignore_missing_imports = true no_implicit_optional = true sqlite_cache = true strict = true strict_equality = true warn_no_return = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true plugins = ["pydantic.mypy"] [tool.bandit] exclude_dirs = ["./test"] [tool.ruff.lint] extend-select = ["I", "UP"] ignore = [ "UP007", # https://github.com/pydantic/pydantic/issues/4146 "UP011", "UP015", ] ================================================ FILE: sigstore/__init__.py ================================================ # 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. """ The `sigstore` Python APIs. For command-line usage of `sigstore`, refer to the `sigstore` [README](https://github.com/sigstore/sigstore-python). Otherwise, here are some quick starting points: * `sigstore.verify`: verifying of Sigstore signatures, including flexible policy control * `sigstore.sign`: creation of Sigstore signatures """ __version__ = "4.2.0" ================================================ FILE: sigstore/__main__.py ================================================ # 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. """ The `python -m sigstore` entrypoint. """ if __name__ == "__main__": # pragma: no cover from sigstore._cli import main main() ================================================ FILE: sigstore/_cli.py ================================================ # 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. from __future__ import annotations import argparse import base64 import json import logging import os import sys from concurrent import futures from dataclasses import dataclass from pathlib import Path from typing import Any, NoReturn, TypeAlias, Union from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509 import load_pem_x509_certificate from pydantic import ValidationError from rich.console import Console from rich.logging import RichHandler from sigstore_models.bundle.v1 import Bundle as RawBundle from sigstore_models.common.v1 import HashAlgorithm from sigstore import __version__, dsse from sigstore._internal.fulcio.client import ExpiredCertificate from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._utils import sha256_digest from sigstore.dsse import StatementBuilder, Subject from sigstore.dsse._predicate import ( PredicateType, SLSAPredicateV0_2, SLSAPredicateV1_0, ) from sigstore.errors import CertValidationError, Error, VerificationError from sigstore.hashes import Hashed from sigstore.models import Bundle, ClientTrustConfig, InvalidBundle from sigstore.oidc import ( ExpiredIdentity, IdentityToken, Issuer, detect_credential, ) from sigstore.sign import Signer, SigningContext from sigstore.verify import ( Verifier, policy, ) _console = Console(file=sys.stderr) logging.basicConfig( format="%(message)s", datefmt="[%X]", handlers=[RichHandler(console=_console)] ) _logger = logging.getLogger(__name__) # NOTE: We configure the top package logger, rather than the root logger, # to avoid overly verbose logging in third-party code by default. _package_logger = logging.getLogger("sigstore") _package_logger.setLevel(os.environ.get("SIGSTORE_LOGLEVEL", "INFO").upper()) @dataclass(frozen=True) class SigningOutputs: signature: Path | None = None certificate: Path | None = None bundle: Path | None = None @dataclass(frozen=True) class VerificationUnbundledMaterials: certificate: Path signature: Path @dataclass(frozen=True) class VerificationBundledMaterials: bundle: Path VerificationMaterials: TypeAlias = Union[ VerificationUnbundledMaterials, VerificationBundledMaterials ] # Map of inputs -> outputs for signing operations OutputMap: TypeAlias = dict[Path, SigningOutputs] def _fatal(message: str) -> NoReturn: """ Logs a fatal condition and exits. """ _logger.fatal(message) sys.exit(1) def _invalid_arguments(args: argparse.Namespace, message: str) -> NoReturn: """ An `argparse` helper that fixes up the type hints on our use of `ArgumentParser.error`. """ args._parser.error(message) raise ValueError("unreachable") def _boolify_env(envvar: str) -> bool: """ An `argparse` helper for turning an environment variable into a boolean. The semantics here closely mirror `distutils.util.strtobool`. See: """ val = os.getenv(envvar) if val is None: return False val = val.lower() if val in {"y", "yes", "true", "t", "on", "1"}: return True elif val in {"n", "no", "false", "f", "off", "0"}: return False else: raise ValueError(f"can't coerce '{val}' to a boolean") def _add_shared_verify_input_options(group: argparse._ArgumentGroup) -> None: """ Common input options, shared between all `sigstore verify` subcommands. """ group.add_argument( "--certificate", "--cert", metavar="FILE", type=Path, default=os.getenv("SIGSTORE_CERTIFICATE"), help="The PEM-encoded certificate to verify against; not used with multiple inputs", ) group.add_argument( "--signature", metavar="FILE", type=Path, default=os.getenv("SIGSTORE_SIGNATURE"), help="The signature to verify against; not used with multiple inputs", ) group.add_argument( "--bundle", metavar="FILE", type=Path, default=os.getenv("SIGSTORE_BUNDLE"), help=("The Sigstore bundle to verify with; not used with multiple inputs"), ) def file_or_digest(arg: str) -> Hashed | Path: path = Path(arg) if path.is_file(): return path elif arg.startswith("sha256"): digest = bytes.fromhex(arg[len("sha256:") :]) if len(digest) != 32: raise ValueError return Hashed( digest=digest, algorithm=HashAlgorithm.SHA2_256, ) else: raise ValueError group.add_argument( "files_or_digest", metavar="FILE_OR_DIGEST", type=file_or_digest, nargs="+", help="The file path or the digest to verify. The digest should start with the 'sha256:' prefix.", ) def _add_shared_verification_options(group: argparse._ArgumentGroup) -> None: group.add_argument( "--offline", action="store_true", default=_boolify_env("SIGSTORE_OFFLINE"), help="Perform offline verification; requires a Sigstore bundle", ) def _add_shared_oidc_options( group: argparse._ArgumentGroup | argparse.ArgumentParser, ) -> None: """ Common OIDC options, shared between `sigstore sign` and `sigstore get-identity-token`. """ group.add_argument( "--oidc-client-id", metavar="ID", type=str, default=os.getenv("SIGSTORE_OIDC_CLIENT_ID", "sigstore"), help="The custom OpenID Connect client ID to use during OAuth2", ) group.add_argument( "--oidc-client-secret", metavar="SECRET", type=str, default=os.getenv("SIGSTORE_OIDC_CLIENT_SECRET"), help="The custom OpenID Connect client secret to use during OAuth2", ) group.add_argument( "--oidc-disable-ambient-providers", action="store_true", default=_boolify_env("SIGSTORE_OIDC_DISABLE_AMBIENT_PROVIDERS"), help="Disable ambient OpenID Connect credential detection (e.g. on GitHub Actions)", ) group.add_argument( "--oidc-issuer", metavar="URL", type=str, default=os.getenv("SIGSTORE_OIDC_ISSUER", None), help="The OpenID Connect issuer to use", ) group.add_argument( "--oauth-force-oob", action="store_true", default=_boolify_env("SIGSTORE_OAUTH_FORCE_OOB"), help="Force an out-of-band OAuth flow and do not automatically start the default web browser", ) def _parser() -> argparse.ArgumentParser: # Arguments in parent_parser can be used for both commands and subcommands parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( "-v", "--verbose", action="count", default=0, help="run with additional debug logging; supply multiple times to increase verbosity", ) parser = argparse.ArgumentParser( prog="sigstore", description="a tool for signing and verifying Python package distributions", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) parser.add_argument( "-V", "--version", action="version", version=f"sigstore {__version__}" ) global_instance_options = parser.add_mutually_exclusive_group() global_instance_options.add_argument( "--staging", action="store_true", default=_boolify_env("SIGSTORE_STAGING"), help=( "Use sigstore's staging instance, instead of the default production instance." " Mutually exclusive with other instance configuration arguments." ), ) global_instance_options.add_argument( "--instance", metavar="URL", type=str, help=( "Use a given Sigstore instance URL, instead of the default production instance." " Mutually exclusive with other instance configuration arguments." ), ) global_instance_options.add_argument( "--trust-config", metavar="FILE", type=Path, help=( "Use given client trust configuration instead of using the default production" " instance. Mutually exclusive with other instance configuration arguments." ), ) subcommands = parser.add_subparsers( required=True, dest="subcommand", metavar="COMMAND", help="the operation to perform", ) # `sigstore attest` attest = subcommands.add_parser( "attest", help="sign one or more inputs using DSSE", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) attest.add_argument( "--rekor-version", type=int, metavar="VERSION", default=argparse.SUPPRESS, help="Force the rekor transparency log version. Valid values are [1, 2]. By default the highest available version is used", ) attest.add_argument( "files", metavar="FILE", type=Path, nargs="+", help="The file to sign", ) dsse_options = attest.add_argument_group("DSSE options") dsse_options.add_argument( "--predicate", metavar="FILE", type=Path, required=True, help="Path to the predicate file", ) dsse_options.add_argument( "--predicate-type", metavar="TYPE", choices=list(PredicateType), type=PredicateType, required=True, help=f"Specify a predicate type ({', '.join(list(PredicateType))})", ) oidc_options = attest.add_argument_group("OpenID Connect options") oidc_options.add_argument( "--identity-token", metavar="TOKEN", type=str, default=os.getenv("SIGSTORE_IDENTITY_TOKEN"), help="the OIDC identity token to use", ) _add_shared_oidc_options(oidc_options) output_options = attest.add_argument_group("Output options") output_options.add_argument( "--bundle", metavar="FILE", type=Path, default=os.getenv("SIGSTORE_BUNDLE"), help=( "Write a single Sigstore bundle to the given file; does not work with multiple input " "files" ), ) output_options.add_argument( "--overwrite", action="store_true", default=_boolify_env("SIGSTORE_OVERWRITE"), help="Overwrite preexisting bundle outputs, if present", ) # `sigstore sign` sign = subcommands.add_parser( "sign", help="sign one or more inputs", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) sign.add_argument( "--rekor-version", type=int, metavar="VERSION", default=argparse.SUPPRESS, help="Force the rekor transparency log version. Valid values are [1, 2]. By default the highest available version is used", ) oidc_options = sign.add_argument_group("OpenID Connect options") oidc_options.add_argument( "--identity-token", metavar="TOKEN", type=str, default=os.getenv("SIGSTORE_IDENTITY_TOKEN"), help="the OIDC identity token to use", ) _add_shared_oidc_options(oidc_options) output_options = sign.add_argument_group("Output options") output_options.add_argument( "--no-default-files", action="store_true", default=_boolify_env("SIGSTORE_NO_DEFAULT_FILES"), help="Don't emit the default output files ({input}.sigstore.json)", ) output_options.add_argument( "--signature", "--output-signature", metavar="FILE", type=Path, default=os.getenv("SIGSTORE_OUTPUT_SIGNATURE"), help=( "Write a single signature to the given file; does not work with multiple input files" ), ) output_options.add_argument( "--certificate", "--output-certificate", metavar="FILE", type=Path, default=os.getenv("SIGSTORE_OUTPUT_CERTIFICATE"), help=( "Write a single certificate to the given file; does not work with multiple input files" ), ) output_options.add_argument( "--bundle", metavar="FILE", type=Path, default=os.getenv("SIGSTORE_BUNDLE"), help=( "Write a single Sigstore bundle to the given file; does not work with multiple input " "files" ), ) output_options.add_argument( "--output-directory", metavar="DIR", type=Path, default=os.getenv("SIGSTORE_OUTPUT_DIRECTORY"), help=( "Write default outputs to the given directory (conflicts with --signature, --certificate" ", --bundle)" ), ) output_options.add_argument( "--overwrite", action="store_true", default=_boolify_env("SIGSTORE_OVERWRITE"), help="Overwrite preexisting signature and certificate outputs, if present", ) sign.add_argument( "files", metavar="FILE", type=Path, nargs="+", help="The file to sign", ) # `sigstore verify` verify = subcommands.add_parser( "verify", help="verify one or more inputs", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) verify_subcommand = verify.add_subparsers( required=True, dest="verify_subcommand", metavar="COMMAND", help="the kind of verification to perform", ) # `sigstore verify identity` verify_identity = verify_subcommand.add_parser( "identity", help="verify against a known identity and identity provider", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) input_options = verify_identity.add_argument_group("Verification inputs") _add_shared_verify_input_options(input_options) verification_options = verify_identity.add_argument_group("Verification options") _add_shared_verification_options(verification_options) verification_options.add_argument( "--cert-identity", metavar="IDENTITY", type=str, default=os.getenv("SIGSTORE_CERT_IDENTITY"), help="The identity to check for in the certificate's Subject Alternative Name", required=True, ) verification_options.add_argument( "--cert-oidc-issuer", metavar="URL", type=str, default=os.getenv("SIGSTORE_CERT_OIDC_ISSUER"), help="The OIDC issuer URL to check for in the certificate's OIDC issuer extension", required=True, ) # `sigstore verify github` verify_github = verify_subcommand.add_parser( "github", help="verify against GitHub Actions-specific claims", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) input_options = verify_github.add_argument_group("Verification inputs") _add_shared_verify_input_options(input_options) verification_options = verify_github.add_argument_group("Verification options") _add_shared_verification_options(verification_options) verification_options.add_argument( "--cert-identity", metavar="IDENTITY", type=str, default=os.getenv("SIGSTORE_CERT_IDENTITY"), help="The identity to check for in the certificate's Subject Alternative Name", ) verification_options.add_argument( "--trigger", dest="workflow_trigger", metavar="EVENT", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_TRIGGER"), help="The GitHub Actions event name that triggered the workflow", ) verification_options.add_argument( "--sha", dest="workflow_sha", metavar="SHA", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_SHA"), help="The `git` commit SHA that the workflow run was invoked with", ) verification_options.add_argument( "--name", dest="workflow_name", metavar="NAME", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_NAME"), help="The name of the workflow that was triggered", ) verification_options.add_argument( "--repository", dest="workflow_repository", metavar="REPO", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_REPOSITORY"), help="The repository slug that the workflow was triggered under", ) verification_options.add_argument( "--ref", dest="workflow_ref", metavar="REF", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_REF"), help="The `git` ref that the workflow was invoked with", ) # `sigstore get-identity-token` get_identity_token = subcommands.add_parser( "get-identity-token", help="retrieve and return a Sigstore-compatible OpenID Connect token", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) _add_shared_oidc_options(get_identity_token) # `sigstore trust-instance` trust_instance = subcommands.add_parser( "trust-instance", help="Initialize trust for a Sigstore instance", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) trust_instance.add_argument( "root", metavar="ROOT", type=Path, help="The TUF root metadata for the instance", ) # `sigstore plumbing` plumbing = subcommands.add_parser( "plumbing", help="developer-only plumbing operations", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) plumbing_subcommands = plumbing.add_subparsers( required=True, dest="plumbing_subcommand", metavar="COMMAND", help="the operation to perform", ) # `sigstore plumbing fix-bundle` fix_bundle = plumbing_subcommands.add_parser( "fix-bundle", help="fix (and optionally upgrade) older bundle formats", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) fix_bundle.add_argument( "--bundle", metavar="FILE", type=Path, required=True, help=("The bundle to fix and/or upgrade"), ) fix_bundle.add_argument( "--upgrade-version", action="store_true", help="Upgrade the bundle to the latest bundle spec version", ) fix_bundle.add_argument( "--in-place", action="store_true", help="Overwrite the input bundle with its fix instead of emitting to stdout", ) # `sigstore plumbing update-trust-root` plumbing_subcommands.add_parser( "update-trust-root", help="update the local trust root to the latest version via TUF", formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[parent_parser], ) return parser def main(args: list[str] | None = None) -> None: if not args: args = sys.argv[1:] parser = _parser() args = parser.parse_args(args) # Configure logging upfront, so that we don't miss anything. if args.verbose >= 1: _package_logger.setLevel("DEBUG") if args.verbose >= 2: logging.getLogger().setLevel("DEBUG") _logger.debug(f"parsed arguments {args}") # Stuff the parser back into our namespace, so that we can use it for # error handling later. args._parser = parser try: if args.subcommand == "sign": _sign(args) elif args.subcommand == "attest": _attest(args) elif args.subcommand == "verify": if args.verify_subcommand == "identity": _verify_identity(args) elif args.verify_subcommand == "github": _verify_github(args) elif args.subcommand == "get-identity-token": _get_identity_token(args) elif args.subcommand == "trust-instance": _trust_instance(args) elif args.subcommand == "plumbing": if args.plumbing_subcommand == "fix-bundle": _fix_bundle(args) elif args.plumbing_subcommand == "update-trust-root": _update_trust_root(args) else: _invalid_arguments(args, f"Unknown subcommand: {args.subcommand}") except Error as e: e.log_and_exit(_logger, args.verbose >= 1) def _trust_instance(args: argparse.Namespace) -> None: """ Initialize trust for a Sigstore instance """ root: Path = args.root instance: str | None = args.instance if not root.is_file(): _invalid_arguments(args, f"Input must be a file: {root}") if instance is None: _invalid_arguments(args, "trust-instance requires '--instance URL'") # ClientTrustConfig construction verifies the root is valid, and # stores it in the local metadata store for future use _ = ClientTrustConfig.from_tuf(instance, bootstrap_root=root) def _get_identity_token(args: argparse.Namespace) -> None: """ Output the OIDC authentication token """ identity = _get_identity(args, _get_trust_config(args)) if identity: print(identity) else: _invalid_arguments(args, "No identity token supplied or detected!") def _sign_file_threaded( signer: Signer, predicate_type: str | None, predicate: dict[str, Any] | None, file: Path, outputs: SigningOutputs, ) -> None: """sign method to be called from signing thread""" _logger.debug(f"signing for {file.name}") with file.open(mode="rb") as io: # The input can be indefinitely large, so we perform a streaming # digest and sign the prehash rather than buffering it fully. digest = sha256_digest(io) try: if predicate is None: result = signer.sign_artifact(input_=digest) else: subject = Subject(name=file.name, digest={"sha256": digest.digest.hex()}) statement_builder = StatementBuilder( subjects=[subject], predicate_type=predicate_type, predicate=predicate, ) result = signer.sign_dsse(statement_builder.build()) except ExpiredIdentity as exp_identity: _logger.error("Signature failed: identity token has expired") raise exp_identity except ExpiredCertificate as exp_certificate: _logger.error("Signature failed: Fulcio signing certificate has expired") raise exp_certificate _logger.info( f"Transparency log entry created at index: {result.log_entry._inner.log_index}" ) if outputs.signature is not None: signature = base64.b64encode(result.signature).decode() with outputs.signature.open(mode="w", encoding="utf-8") as io: print(signature, file=io) if outputs.certificate is not None: cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode() with outputs.certificate.open(mode="w", encoding="utf-8") as io: print(cert_pem, file=io) if outputs.bundle is not None: with outputs.bundle.open(mode="w", encoding="utf-8") as io: print(result.to_json(), file=io) def _sign_common( args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None ) -> None: """ Signing logic for both `sigstore sign` and `sigstore attest` Both `sign` and `attest` share the same signing logic, the only change is whether they sign over a DSSE envelope or a hashedrekord. This function differentiates between the two using the `predicate` argument. If present, it will generate an in-toto statement and wrap it in a DSSE envelope. If not, it will use a hashedrekord. """ # Select the signing context to use. trust_config = _get_trust_config(args) signing_ctx = SigningContext.from_trust_config(trust_config) # The order of precedence for identities is as follows: # # 1) Explicitly supplied identity token # 2) Ambient credential detected in the environment, unless disabled # 3) Interactive OAuth flow identity: IdentityToken | None if args.identity_token: identity = IdentityToken(args.identity_token, args.oidc_client_id) else: identity = _get_identity(args, trust_config) if not identity: _invalid_arguments(args, "No identity token supplied or detected!") # Not all commands provide --predicate-type predicate_type = getattr(args, "predicate_type", None) with signing_ctx.signer(identity) as signer: print("Using ephemeral certificate:") cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode() print(cert_pem) # sign in threads: this is relevant for especially Rekor v2 as otherwise we wait # for log inclusion for each signature separately with futures.ThreadPoolExecutor() as executor: jobs = [ executor.submit( _sign_file_threaded, signer, predicate_type, predicate, file, outputs, ) for file, outputs in output_map.items() ] for job in futures.as_completed(jobs): job.result() for file, outputs in output_map.items(): if outputs.signature is not None: print(f"Signature written to {outputs.signature}") if outputs.certificate is not None: print(f"Certificate written to {outputs.certificate}") if outputs.bundle is not None: print(f"Sigstore bundle written to {outputs.bundle}") def _attest(args: argparse.Namespace) -> None: predicate_path = args.predicate if not predicate_path.is_file(): _invalid_arguments(args, f"Predicate must be a file: {predicate_path}") try: with open(predicate_path, "r", encoding="utf-8") as f: predicate = json.load(f) # We do a basic sanity check using our Pydantic models to see if the # contents of the predicate file match the specified predicate type. # Since most of the predicate fields are optional, this only checks that # the fields that are present and correctly spelled have the expected # type. if args.predicate_type == PredicateType.SLSA_v0_2: SLSAPredicateV0_2.model_validate(predicate) elif args.predicate_type == PredicateType.SLSA_v1_0: SLSAPredicateV1_0.model_validate(predicate) else: _invalid_arguments( args, f'Unsupported predicate type "{args.predicate_type}". Predicate type must be one of: {list(PredicateType)}', ) except (ValidationError, json.JSONDecodeError) as e: _invalid_arguments( args, f'Unable to parse predicate of type "{args.predicate_type}": {e}' ) # Build up the map of inputs -> outputs ahead of any signing operations, # so that we can fail early if overwriting without `--overwrite`. output_map: OutputMap = {} for file in args.files: if not file.is_file(): _invalid_arguments(args, f"Input must be a file: {file}") bundle = args.bundle output_dir = file.parent if not bundle: bundle = output_dir / f"{file.name}.sigstore.json" if bundle and bundle.exists() and not args.overwrite: _invalid_arguments( args, f"Refusing to overwrite outputs without --overwrite: {bundle}", ) output_map[file] = SigningOutputs(bundle=bundle) # We sign the contents of the predicate file, rather than signing the Pydantic # model's JSON dump. This is because doing a JSON -> Model -> JSON roundtrip might # change the original predicate if it doesn't match exactly our Pydantic model # (e.g.: if it has extra fields). _sign_common(args, output_map=output_map, predicate=predicate) def _sign(args: argparse.Namespace) -> None: has_sig = bool(args.signature) has_crt = bool(args.certificate) has_bundle = bool(args.bundle) # `--no-default-files` has no effect on `--bundle`, but we forbid it because # it indicates user confusion. if args.no_default_files and has_bundle: _invalid_arguments( args, "--no-default-files may not be combined with --bundle." ) # Fail if `--signature` or `--certificate` is specified *and* we have more # than one input. if (has_sig or has_crt or has_bundle) and len(args.files) > 1: _invalid_arguments( args, "Error: --signature, --certificate, and --bundle can't be used with " "explicit outputs for multiple inputs.", ) if args.output_directory and (has_sig or has_crt or has_bundle): _invalid_arguments( args, "Error: --signature, --certificate, and --bundle can't be used with " "an explicit output directory.", ) # Fail if either `--signature` or `--certificate` is specified, but not both. if has_sig ^ has_crt: _invalid_arguments( args, "Error: --signature and --certificate must be used together." ) # Build up the map of inputs -> outputs ahead of any signing operations, # so that we can fail early if overwriting without `--overwrite`. output_map: OutputMap = {} for file in args.files: if not file.is_file(): _invalid_arguments(args, f"Input must be a file: {file}") sig, cert, bundle = ( args.signature, args.certificate, args.bundle, ) output_dir = args.output_directory or file.parent if output_dir.exists() and not output_dir.is_dir(): _invalid_arguments( args, f"Output directory exists and is not a directory: {output_dir}" ) output_dir.mkdir(parents=True, exist_ok=True) if not bundle and not args.no_default_files: bundle = output_dir / f"{file.name}.sigstore.json" if not args.overwrite: extants = [] if sig and sig.exists(): extants.append(str(sig)) if cert and cert.exists(): extants.append(str(cert)) if bundle and bundle.exists(): extants.append(str(bundle)) if extants: _invalid_arguments( args, "Refusing to overwrite outputs without --overwrite: " f"{', '.join(extants)}", ) output_map[file] = SigningOutputs( signature=sig, certificate=cert, bundle=bundle ) _sign_common(args, output_map=output_map, predicate=None) def _collect_verification_state( args: argparse.Namespace, ) -> tuple[Verifier, list[tuple[Path | Hashed, Hashed, Bundle]]]: """ Performs CLI functionality common across all `sigstore verify` subcommands. Returns a tuple of the active verifier instance and a list of `(path, hashed, bundle)` tuples, where `path` is the filename for display purposes, `hashed` is the pre-hashed input to the file being verified and `bundle` is the `Bundle` to verify with. """ # Fail if --certificate, --signature, or --bundle is specified, and we # have more than one input. if (args.certificate or args.signature or args.bundle) and len( args.files_or_digest ) > 1: _invalid_arguments( args, "--certificate, --signature, or --bundle can only be used " "with a single input file or digest", ) # Fail if `--certificate` or `--signature` is used with `--bundle`. if args.bundle and (args.certificate or args.signature): _invalid_arguments( args, "--bundle cannot be used with --certificate or --signature" ) # Fail if digest input is not used with `--bundle` or both `--certificate` and `--signature`. if any(isinstance(x, Hashed) for x in args.files_or_digest): if not args.bundle and not (args.certificate and args.signature): _invalid_arguments( args, "verifying a digest input (sha256:*) needs either --bundle or both --certificate and --signature", ) # Fail if `--certificate` or `--signature` is used with `--offline`. if args.offline and (args.certificate or args.signature): _invalid_arguments( args, "--offline cannot be used with --certificate or --signature" ) # The converse of `sign`: we build up an expected input map and check # that we have everything so that we can fail early. input_map: dict[Path | Hashed, VerificationMaterials] = {} for file in (f for f in args.files_or_digest if isinstance(f, Path)): if not file.is_file(): _invalid_arguments(args, f"Input must be a file: {file}") sig, cert, bundle = ( args.signature, args.certificate, args.bundle, ) if sig is None: sig = file.parent / f"{file.name}.sig" if cert is None: cert = file.parent / f"{file.name}.crt" if bundle is None: # NOTE(ww): If the user hasn't specified a bundle via `--bundle` and # `{input}.sigstore.json` doesn't exist, then we try `{input}.sigstore` # for backwards compatibility. legacy_default_bundle = file.parent / f"{file.name}.sigstore" bundle = file.parent / f"{file.name}.sigstore.json" if not bundle.is_file() and legacy_default_bundle.is_file(): if not cert.is_file() or not sig.is_file(): # NOTE(ww): Only show this warning if bare materials # are not provided, since bare materials take precedence over # a .sigstore bundle. _logger.warning( f"{file}: {legacy_default_bundle} should be named {bundle}. " "Support for discovering 'bare' .sigstore inputs will be deprecated in " "a future release." ) bundle = legacy_default_bundle elif bundle.is_file() and legacy_default_bundle.is_file(): # Don't allow the user to implicitly verify `{input}.sigstore.json` if # `{input}.sigstore` is also present, since this implies user confusion. _invalid_arguments( args, f"Conflicting inputs: {bundle} and {legacy_default_bundle}", ) missing = [] if args.signature or args.certificate: if not sig.is_file(): missing.append(str(sig)) if not cert.is_file(): missing.append(str(cert)) input_map[file] = VerificationUnbundledMaterials( certificate=cert, signature=sig ) else: # If a user hasn't explicitly supplied `--signature` or `--certificate`, # we expect a bundle either supplied via `--bundle` or with the # default `{input}.sigstore(.json)?` name. if not bundle.is_file(): missing.append(str(bundle)) input_map[file] = VerificationBundledMaterials(bundle=bundle) if missing: _invalid_arguments( args, f"Missing verification materials for {(file)}: {', '.join(missing)}", ) if not input_map: if len(args.files_or_digest) != 1: # This should never happen, since if `input_map` is empty that means there # were no file inputs, and therefore exactly one digest input should be # present. _invalid_arguments( args, "Internal error: Found multiple digests in CLI arguments" ) hashed = args.files_or_digest[0] sig, cert, bundle = ( args.signature, args.certificate, args.bundle, ) missing = [] if args.signature or args.certificate: if not sig.is_file(): missing.append(str(sig)) if not cert.is_file(): missing.append(str(cert)) input_map[hashed] = VerificationUnbundledMaterials( certificate=cert, signature=sig ) else: # If a user hasn't explicitly supplied `--signature` or `--certificate`, # we expect a bundle supplied via `--bundle` if not bundle.is_file(): missing.append(str(bundle)) input_map[hashed] = VerificationBundledMaterials(bundle=bundle) if missing: _invalid_arguments( args, f"Missing verification materials for {(hashed)}: {', '.join(missing)}", ) trust_config = _get_trust_config(args) verifier = Verifier(trusted_root=trust_config.trusted_root) all_materials = [] for file_or_hashed, materials in input_map.items(): if isinstance(file_or_hashed, Path): with file_or_hashed.open(mode="rb") as io: hashed = sha256_digest(io) else: hashed = file_or_hashed if isinstance(materials, VerificationBundledMaterials): # Load the bundle _logger.debug(f"Using bundle from: {materials.bundle}") bundle_bytes = materials.bundle.read_bytes() bundle = Bundle.from_json(bundle_bytes) else: # Load the signing certificate _logger.debug(f"Using certificate from: {materials.certificate}") cert = load_pem_x509_certificate(materials.certificate.read_bytes()) # Load the signature _logger.debug(f"Using signature from: {materials.signature}") b64_signature = materials.signature.read_text(encoding="utf-8") signature = base64.b64decode(b64_signature) # When using "detached" materials, we *must* retrieve the log # entry from the online log. # TODO: This should be abstracted somewhere much better. log_entry = verifier._rekor.log.entries.retrieve.post( _hashedrekord_from_parts(cert, signature, hashed) ) if log_entry is None: _invalid_arguments( args, f"No matching log entry for {file_or_hashed}'s verification materials", ) bundle = Bundle.from_parts(cert, signature, log_entry) _logger.debug(f"Verifying contents from: {file_or_hashed}") all_materials.append((file_or_hashed, hashed, bundle)) return (verifier, all_materials) def _verify_identity(args: argparse.Namespace) -> None: verifier, materials = _collect_verification_state(args) for file_or_digest, hashed, bundle in materials: policy_ = policy.Identity( identity=args.cert_identity, issuer=args.cert_oidc_issuer, ) try: statement = _verify_common(verifier, hashed, bundle, policy_) print(f"OK: {file_or_digest}", file=sys.stderr) if statement is not None: print(statement._contents.decode()) except Error as exc: if isinstance(exc, CertValidationError): _logger.warning( "A certificate chain was not valid, are you using the correct Sigstore instance?" ) _logger.error(f"FAIL: {file_or_digest}") exc.log_and_exit(_logger, args.verbose >= 1) def _verify_github(args: argparse.Namespace) -> None: inner_policies: list[policy.VerificationPolicy] = [] # We require at least one of `--cert-identity` or `--repository`, # to minimize the risk of user confusion about what's being verified. if not (args.cert_identity or args.workflow_repository): _invalid_arguments(args, "--cert-identity or --repository is required") # No matter what the user configures above, we require the OIDC issuer to # be GitHub Actions. inner_policies.append( policy.OIDCIssuer("https://token.actions.githubusercontent.com") ) if args.cert_identity: inner_policies.append( policy.Identity( identity=args.cert_identity, # We always explicitly check the issuer below, so configuring # it here is unnecessary. issuer=None, ) ) if args.workflow_trigger: inner_policies.append(policy.GitHubWorkflowTrigger(args.workflow_trigger)) if args.workflow_sha: inner_policies.append(policy.GitHubWorkflowSHA(args.workflow_sha)) if args.workflow_name: inner_policies.append(policy.GitHubWorkflowName(args.workflow_name)) if args.workflow_repository: inner_policies.append(policy.GitHubWorkflowRepository(args.workflow_repository)) if args.workflow_ref: inner_policies.append(policy.GitHubWorkflowRef(args.workflow_ref)) policy_ = policy.AllOf(inner_policies) verifier, materials = _collect_verification_state(args) for file_or_digest, hashed, bundle in materials: try: statement = _verify_common(verifier, hashed, bundle, policy_) print(f"OK: {file_or_digest}", file=sys.stderr) if statement is not None: print(statement._contents) except Error as exc: if isinstance(exc, CertValidationError): _logger.warning( "A certificate chain was not valid, are you using the correct Sigstore instance?" ) _logger.error(f"FAIL: {file_or_digest}") exc.log_and_exit(_logger, args.verbose >= 1) def _verify_common( verifier: Verifier, hashed: Hashed, bundle: Bundle, policy_: policy.VerificationPolicy, ) -> dsse.Statement | None: """ Common verification handling. This dispatches to either artifact or DSSE verification, depending on `bundle`'s inner type. If verifying a DSSE envelope, return the wrapped in-toto statement if verification succeeds """ # If the bundle specifies a DSSE envelope, perform DSSE verification # and assert that the inner payload is an in-toto statement bound # to a subject matching the input's digest. if bundle._dsse_envelope: type_, payload = verifier.verify_dsse(bundle=bundle, policy=policy_) if type_ != dsse.Envelope._TYPE: raise VerificationError(f"expected JSON payload for DSSE, got {type_}") stmt = dsse.Statement(payload) if not stmt._matches_digest(hashed): raise VerificationError( f"in-toto statement has no subject for digest {hashed.digest.hex()}" ) return stmt else: verifier.verify_artifact( input_=hashed, bundle=bundle, policy=policy_, ) return None def _get_trust_config(args: argparse.Namespace) -> ClientTrustConfig: """ Return the client trust configuration (Sigstore service URLs, key material and lifetimes) The configuration may come from explicit argument (--trust-config) or from the TUF repository of the used Sigstore instance (--staging or --instance). """ # Not all commands provide --offline offline = getattr(args, "offline", False) if args.trust_config: trust_config = ClientTrustConfig.from_json(args.trust_config.read_text()) elif args.instance: trust_config = ClientTrustConfig.from_tuf(args.instance, offline=offline) elif args.staging: trust_config = ClientTrustConfig.staging(offline=offline) else: trust_config = ClientTrustConfig.production(offline=offline) # Enforce rekor version if --rekor-version is used trust_config.force_tlog_version = getattr(args, "rekor_version", None) return trust_config def _get_identity( args: argparse.Namespace, trust_config: ClientTrustConfig ) -> IdentityToken | None: token = None if not args.oidc_disable_ambient_providers: token = detect_credential(args.oidc_client_id) # Happy path: we've detected an ambient credential, so we can return early. if token: return IdentityToken(token, args.oidc_client_id) if args.oidc_issuer is not None: issuer = Issuer(args.oidc_issuer) else: issuer = Issuer(trust_config.signing_config.get_oidc_url()) if args.oidc_client_secret is None: args.oidc_client_secret = "" # nosec: B105 token = issuer.identity_token( client_id=args.oidc_client_id, client_secret=args.oidc_client_secret, force_oob=args.oauth_force_oob, ) return token def _fix_bundle(args: argparse.Namespace) -> None: # NOTE: We could support `--trusted-root` here in the future, # for custom Rekor instances. rekor = RekorClient.staging() if args.staging else RekorClient.production() raw_bundle = RawBundle.from_json(args.bundle.read_bytes()) if len(raw_bundle.verification_material.tlog_entries) != 1: _fatal("unfixable bundle: must have exactly one log entry") # Some old versions of sigstore-python (1.x) produce malformed # bundles where the inclusion proof is present but without # its checkpoint. We fix these by retrieving the complete entry # from Rekor and replacing the incomplete entry. tlog_entry = raw_bundle.verification_material.tlog_entries[0] inclusion_proof = tlog_entry.inclusion_proof if not inclusion_proof.checkpoint: _logger.info("fixable: bundle's log entry is missing a checkpoint") new_entry = rekor.log.entries.get(log_index=tlog_entry.log_index) raw_bundle.verification_material.tlog_entries = [new_entry._inner] # Try to create our invariant-preserving Bundle from the any changes above. try: bundle = Bundle(raw_bundle) except InvalidBundle as e: e.log_and_exit(_logger) # Round-trip through the bundle's parts to induce a version upgrade, # if requested. if args.upgrade_version: bundle = Bundle._from_parts(*bundle._to_parts()) if args.in_place: args.bundle.write_text(bundle.to_json()) else: print(bundle.to_json()) def _update_trust_root(args: argparse.Namespace) -> None: # Simply creating the TrustConfig in online mode is enough to perform # a metadata update. config = _get_trust_config(args) _console.print( f"Trust root & signing config updated: {len(config.trusted_root.get_fulcio_certs())} Fulcio certificates" ) ================================================ FILE: sigstore/_internal/__init__.py ================================================ # 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. """ sigstore-python's internal APIs. Everything in these APIs is considered internal and unstable, and is not subject to any stability guarantees. """ from requests import __version__ as requests_version from sigstore import __version__ as sigstore_version USER_AGENT = f"sigstore-python/{sigstore_version} (python-requests/{requests_version})" ================================================ FILE: sigstore/_internal/fulcio/__init__.py ================================================ # 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. """ APIs for interacting with Fulcio. """ from .client import ( ExpiredCertificate, FulcioCertificateSigningResponse, FulcioClient, ) __all__ = [ "ExpiredCertificate", "FulcioCertificateSigningResponse", "FulcioClient", ] ================================================ FILE: sigstore/_internal/fulcio/client.py ================================================ # 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. """ Client implementation for interacting with Fulcio. """ from __future__ import annotations import base64 import json import logging from abc import ABC from dataclasses import dataclass from urllib.parse import urljoin import requests from cryptography.hazmat.primitives import serialization from cryptography.x509 import ( Certificate, CertificateSigningRequest, load_pem_x509_certificate, ) from sigstore._internal import USER_AGENT from sigstore._utils import B64Str from sigstore.oidc import IdentityToken _logger = logging.getLogger(__name__) SIGNING_CERT_ENDPOINT = "/api/v2/signingCert" TRUST_BUNDLE_ENDPOINT = "/api/v2/trustBundle" class ExpiredCertificate(Exception): """An error raised when the Certificate is expired.""" @dataclass(frozen=True) class FulcioCertificateSigningResponse: """Certificate response""" cert: Certificate chain: list[Certificate] @dataclass(frozen=True) class FulcioTrustBundleResponse: """Trust bundle response, containing a list of certificate chains""" trust_bundle: list[list[Certificate]] class FulcioClientError(Exception): """ Raised on any error in the Fulcio client. """ pass class _Endpoint(ABC): def __init__(self, url: str, session: requests.Session) -> None: self.url = url self.session = session def _serialize_cert_request(req: CertificateSigningRequest) -> str: data = { "certificateSigningRequest": B64Str( base64.b64encode(req.public_bytes(serialization.Encoding.PEM)).decode() ) } return json.dumps(data) class FulcioSigningCert(_Endpoint): """ Fulcio REST API signing certificate functionality. """ def post( self, req: CertificateSigningRequest, identity: IdentityToken ) -> FulcioCertificateSigningResponse: """ Get the signing certificate, using an X.509 Certificate Signing Request. """ headers = { "Authorization": f"Bearer {identity}", "Content-Type": "application/json", "Accept": "application/pem-certificate-chain", } resp: requests.Response = self.session.post( url=self.url, data=_serialize_cert_request(req), headers=headers ) try: resp.raise_for_status() except requests.HTTPError as http_error: # See if we can optionally add a message if http_error.response: text = json.loads(http_error.response.text) if "message" in http_error.response.text: raise FulcioClientError(text["message"]) from http_error raise FulcioClientError from http_error try: certificates = resp.json()["signedCertificateEmbeddedSct"]["chain"][ "certificates" ] except KeyError: raise FulcioClientError("Fulcio response missing certificate chain") # Cryptography doesn't have chain verification/building built in # https://github.com/pyca/cryptography/issues/2381 if len(certificates) < 2: raise FulcioClientError( f"Certificate chain is too short: {len(certificates)} < 2" ) cert = load_pem_x509_certificate(certificates[0].encode()) chain = [load_pem_x509_certificate(c.encode()) for c in certificates[1:]] return FulcioCertificateSigningResponse(cert, chain) class FulcioTrustBundle(_Endpoint): """ Fulcio REST API trust bundle functionality. """ def get(self) -> FulcioTrustBundleResponse: """Get the certificate chains from Fulcio""" resp: requests.Response = self.session.get(self.url) try: resp.raise_for_status() except requests.HTTPError as http_error: raise FulcioClientError from http_error trust_bundle_json = resp.json() chains: list[list[Certificate]] = [] for certificate_chain in trust_bundle_json["chains"]: chain: list[Certificate] = [] for certificate in certificate_chain["certificates"]: cert: Certificate = load_pem_x509_certificate(certificate.encode()) chain.append(cert) chains.append(chain) return FulcioTrustBundleResponse(chains) class FulcioClient: """The internal Fulcio client""" def __init__(self, url: str) -> None: """Initialize the client""" _logger.debug(f"Fulcio client using URL: {url}") self.url = url self.session = requests.Session() self.session.headers.update( { "User-Agent": USER_AGENT, } ) def __del__(self) -> None: """ Destroys the underlying network session. """ self.session.close() @property def signing_cert(self) -> FulcioSigningCert: """ Returns a model capable of interacting with Fulcio's signing certificate endpoints. """ return FulcioSigningCert( urljoin(self.url, SIGNING_CERT_ENDPOINT), session=self.session ) @property def trust_bundle(self) -> FulcioTrustBundle: """ Returns a model capable of interacting with Fulcio's trust bundle endpoints. """ return FulcioTrustBundle( urljoin(self.url, TRUST_BUNDLE_ENDPOINT), session=self.session ) ================================================ FILE: sigstore/_internal/key_details.py ================================================ # Copyright 2025 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. """ Utilities for getting PublicKeyDetails. """ from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa from cryptography.x509 import Certificate from sigstore_models.common.v1 import PublicKeyDetails def _get_key_details(certificate: Certificate) -> PublicKeyDetails: """ Determine PublicKeyDetails from the Certificate. We disclude the unrecommended types. See - https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md. - https://github.com/sigstore/protobuf-specs/blob/3aaae418f76fb4b34df4def4cd093c464f20fed3/protos/sigstore_common.proto """ public_key = certificate.public_key() params = certificate.signature_algorithm_parameters if isinstance(public_key, ec.EllipticCurvePublicKey): if isinstance(public_key.curve, ec.SECP256R1): key_details = PublicKeyDetails.PKIX_ECDSA_P256_SHA_256 elif isinstance(public_key.curve, ec.SECP384R1): key_details = PublicKeyDetails.PKIX_ECDSA_P384_SHA_384 elif isinstance(public_key.curve, ec.SECP521R1): key_details = PublicKeyDetails.PKIX_ECDSA_P521_SHA_512 else: raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") elif isinstance(public_key, rsa.RSAPublicKey): if public_key.key_size == 2048: if isinstance(params, padding.PKCS1v15): key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256 else: raise ValueError( f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}" ) elif public_key.key_size == 3072: if isinstance(params, padding.PKCS1v15): key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256 else: raise ValueError( f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}" ) elif public_key.key_size == 4096: if isinstance(params, padding.PKCS1v15): key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256 else: raise ValueError( f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}" ) else: raise ValueError(f"Unsupported RSA key size: {public_key.key_size}") elif isinstance(public_key, ed25519.Ed25519PublicKey): key_details = PublicKeyDetails.PKIX_ED25519 # There is likely no need to explicitly detect PKIX_ED25519_PH, especially since the cryptography # library does not yet support Ed25519ph. else: raise ValueError(f"Unsupported public key type: {type(public_key)}") return key_details ================================================ FILE: sigstore/_internal/merkle.py ================================================ # 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. """ Utilities for verifying proof-of-inclusion within Rekor's Merkle Tree. This code is based off Google's Trillian Merkle Tree implementation which Cosign uses to validate Rekor entries. The data format for the Merkle tree nodes is described in IETF's RFC 6962. """ from __future__ import annotations import hashlib import struct import typing from sigstore.errors import VerificationError if typing.TYPE_CHECKING: from sigstore.models import TransparencyLogEntry _LEAF_HASH_PREFIX = 0 _NODE_HASH_PREFIX = 1 def _decomp_inclusion_proof(index: int, size: int) -> tuple[int, int]: """ Breaks down inclusion proof for a leaf at the specified |index| in a tree of the specified |size| into 2 components. The splitting point between them is where paths to leaves |index| and |size-1| diverge. Returns lengths of the bottom and upper proof parts correspondingly. The sum of the two determines the correct length of the inclusion proof. """ inner = (index ^ (size - 1)).bit_length() border = bin(index >> inner).count("1") return inner, border def _chain_inner(seed: bytes, hashes: list[bytes], log_index: int) -> bytes: """ Computes a subtree hash for a node on or below the tree's right border. Assumes |proof| hashes are ordered from lower levels to upper, and |seed| is the initial subtree/leaf hash on the path located at the specified |index| on its level. """ for i in range(len(hashes)): h = hashes[i] if (log_index >> i) & 1 == 0: seed = _hash_children(seed, h) else: seed = _hash_children(h, seed) return seed def _chain_border_right(seed: bytes, hashes: list[bytes]) -> bytes: """ Chains proof hashes along tree borders. This differs from inner chaining because |proof| contains only left-side subtree hashes. """ for h in hashes: seed = _hash_children(h, seed) return seed def _hash_children(lhs: bytes, rhs: bytes) -> bytes: pattern = f"B{len(lhs)}s{len(rhs)}s" data = struct.pack(pattern, _NODE_HASH_PREFIX, lhs, rhs) return hashlib.sha256(data).digest() def _hash_leaf(leaf: bytes) -> bytes: pattern = f"B{len(leaf)}s" data = struct.pack(pattern, _LEAF_HASH_PREFIX, leaf) return hashlib.sha256(data).digest() def verify_merkle_inclusion(entry: TransparencyLogEntry) -> None: """Verify the Merkle Inclusion Proof for a given Rekor entry.""" inclusion_proof = entry._inner.inclusion_proof # Figure out which subset of hashes corresponds to the inner and border nodes. inner, border = _decomp_inclusion_proof( inclusion_proof.log_index, inclusion_proof.tree_size ) # Check against the number of hashes. if len(inclusion_proof.hashes) != (inner + border): raise VerificationError( f"inclusion proof has wrong size: expected {inner + border}, got " f"{len(inclusion_proof.hashes)}" ) # The new entry's hash isn't included in the inclusion proof so we should calculate this # ourselves. leaf_hash: bytes = _hash_leaf(entry._inner.canonicalized_body) # Now chain the hashes belonging to the inner and border portions. We should expect the # calculated hash to match the root hash. intermediate_result: bytes = _chain_inner( leaf_hash, inclusion_proof.hashes[:inner], inclusion_proof.log_index ) calc_hash = _chain_border_right(intermediate_result, inclusion_proof.hashes[inner:]) if calc_hash != inclusion_proof.root_hash: raise VerificationError( f"inclusion proof contains invalid root hash: expected {inclusion_proof}, calculated " f"{calc_hash.hex()}" ) ================================================ FILE: sigstore/_internal/oidc/__init__.py ================================================ # 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. """ Internal OIDC and OAuth functionality for sigstore-python. """ ================================================ FILE: sigstore/_internal/oidc/oauth.py ================================================ # 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. """ OAuth2 flow functionality for `sigstore-python`. """ from __future__ import annotations import base64 import hashlib import http.server import logging import os import threading import urllib.parse import uuid from types import TracebackType from typing import Any, cast from id import IdentityError from sigstore._utils import B64Str from sigstore.oidc import Issuer _logger = logging.getLogger(__name__) # This HTML is copied from the Go Sigstore library and was originally authored by Julien Vermette: # https://github.com/sigstore/sigstore/blob/main/pkg/oauth/interactive.go AUTH_SUCCESS_HTML = """ Sigstore Authentication
sigstore authentication successful!
You may now close this page.
""" class _OAuthFlow: def __init__(self, client_id: str, client_secret: str, issuer: Issuer): self._client_id = client_id self._client_secret = client_secret self._issuer = issuer self._server = _OAuthRedirectServer( self._client_id, self._client_secret, self._issuer ) self._server_thread = threading.Thread( target=lambda server: server.serve_forever(), args=(self._server,), ) def __enter__(self) -> _OAuthRedirectServer: self._server_thread.start() return self._server def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: self._server.shutdown() self._server_thread.join() class _OAuthRedirectHandler(http.server.BaseHTTPRequestHandler): # Short socket timeout to prevent blocking serve_forever() on idle connections timeout = 1 def log_message(self, format: str, *_args: Any) -> None: pass def do_GET(self) -> None: _logger.debug(f"GET: {self.path} with {dict(self.headers)}") server = cast(_OAuthRedirectServer, self.server) # The redirect server only needs one request per connection. # Close conn immediately to prevent keep-alive from blocking. self.close_connection = True # If the auth response has already been populated, the main thread will be stopping this # thread and accessing the auth response shortly so we should stop servicing any requests. if server.auth_response is not None: _logger.debug(f"{self.path} unavailable (teardown)") self.send_response(404) self.end_headers() return None r = urllib.parse.urlsplit(self.path) # We only understand two kinds of requests: # 1. The response from a successful OAuth redirect # 2. The initial request to /, which kicks off (1) if r.path == server.redirect_path: self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") body = AUTH_SUCCESS_HTML.encode("utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) server.auth_response = urllib.parse.parse_qs(r.query) elif r.path == server.auth_request_path: self.send_response(302) self.send_header("Location", server.auth_endpoint) self.end_headers() else: # Anything else sends a "Not Found" response. self.send_response(404) self.end_headers() OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" class _OAuthSession: def __init__(self, client_id: str, client_secret: str, issuer: Issuer): self.__poison = False self._client_id = client_id self._client_secret = client_secret self._issuer = issuer self._state = str(uuid.uuid4()) self.code_verifier = B64Str( base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode() ) @property def state(self) -> str: return self._state @property def code_challenge(self) -> str: return B64Str( base64.urlsafe_b64encode( hashlib.sha256(self.code_verifier.encode()).digest() ) .rstrip(b"=") .decode() ) def auth_endpoint(self, redirect_uri: str) -> str: # Defensive programming: we don't have a nice way to limit the # lifetime of the OAuth session here, so we use the internal # "poison" flag to check if we're attempting to reuse it in a way # that would compromise the flow's security (i.e. state reuse). if self.__poison: raise IdentityError("internal error: OAuth endpoint misuse") else: self.__poison = True params = self._auth_params(redirect_uri) return f"{self._issuer.oidc_config.authorization_endpoint}?{urllib.parse.urlencode(params)}" def _auth_params(self, redirect_uri: str) -> dict[str, Any]: return { "response_type": "code", "client_id": self._client_id, "client_secret": self._client_secret, "scope": "openid email", "redirect_uri": redirect_uri, "code_challenge": self.code_challenge, "code_challenge_method": "S256", "state": self._state, } class _OAuthRedirectServer(http.server.HTTPServer): def __init__(self, client_id: str, client_secret: str, issuer: Issuer) -> None: super().__init__(("localhost", 0), _OAuthRedirectHandler) self.oauth_session = _OAuthSession(client_id, client_secret, issuer) self.auth_response: dict[str, list[str]] | None = None self._is_out_of_band = False @property def base_uri(self) -> str: # NOTE: We'd ideally use `self.server_name` here, but it uses # the FQDN internally (which in turn confuses Sigstore). return f"http://localhost:{self.server_port}" @property def auth_request_path(self) -> str: # TODO: Maybe this should be /auth, for clarity? return "/" @property def redirect_path(self) -> str: return "/auth/callback" @property def redirect_uri(self) -> str: return ( (self.base_uri + self.redirect_path) if not self._is_out_of_band else OOB_REDIRECT_URI ) @property def auth_endpoint(self) -> str: return self.oauth_session.auth_endpoint(self.redirect_uri) def enable_oob(self) -> None: _logger.debug("enabling out-of-band OAuth flow") self._is_out_of_band = True def is_oob(self) -> bool: return self._is_out_of_band ================================================ FILE: sigstore/_internal/rekor/__init__.py ================================================ # 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. """ APIs for interacting with Rekor. """ from __future__ import annotations import base64 import typing from abc import ABC, abstractmethod import rekor_types import requests from cryptography.x509 import Certificate from sigstore._utils import base64_encode_pem_cert from sigstore.dsse import Envelope from sigstore.hashes import Hashed if typing.TYPE_CHECKING: from sigstore.models import TransparencyLogEntry __all__ = [ "_hashedrekord_from_parts", ] EntryRequestBody = typing.NewType("EntryRequestBody", dict[str, typing.Any]) class RekorClientError(Exception): """ A generic error in the Rekor client. """ def __init__(self, http_error: requests.HTTPError): """ Create a new `RekorClientError` from the given `requests.HTTPError`. """ if http_error.response is not None: try: error = rekor_types.Error.model_validate_json(http_error.response.text) super().__init__(f"{error.code}: {error.message}") except Exception: super().__init__( f"Rekor returned an unknown error with HTTP {http_error.response.status_code}" ) else: super().__init__(f"Unexpected Rekor error: {http_error}") class RekorLogSubmitter(ABC): """ Abstract class to represent a Rekor log entry submitter. Intended to be implemented by RekorClient and RekorV2Client. """ @abstractmethod def create_entry( self, request: EntryRequestBody, ) -> TransparencyLogEntry: """ Submit the request to Rekor. """ pass @classmethod @abstractmethod def _build_hashed_rekord_request( self, hashed_input: Hashed, signature: bytes, certificate: Certificate ) -> EntryRequestBody: """ Construct a hashed rekord request to submit to Rekor. """ pass @classmethod @abstractmethod def _build_dsse_request( self, envelope: Envelope, certificate: Certificate ) -> EntryRequestBody: """ Construct a dsse request to submit to Rekor. """ pass # TODO: This should probably live somewhere better. def _hashedrekord_from_parts( cert: Certificate, sig: bytes, hashed: Hashed ) -> rekor_types.Hashedrekord: return rekor_types.Hashedrekord( spec=rekor_types.hashedrekord.HashedrekordV001Schema( signature=rekor_types.hashedrekord.Signature( content=base64.b64encode(sig).decode(), public_key=rekor_types.hashedrekord.PublicKey( content=base64_encode_pem_cert(cert), ), ), data=rekor_types.hashedrekord.Data( hash=rekor_types.hashedrekord.Hash( algorithm=hashed._as_hashedrekord_algorithm(), value=hashed.digest.hex(), ) ), ) ) ================================================ FILE: sigstore/_internal/rekor/checkpoint.py ================================================ # Copyright 2023 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. """ Rekor Checkpoint machinery. """ from __future__ import annotations import base64 import re import struct import typing from dataclasses import dataclass from pydantic import BaseModel, Field, StrictStr from sigstore._utils import KeyID from sigstore.errors import VerificationError if typing.TYPE_CHECKING: from sigstore._internal.trust import RekorKeyring from sigstore.models import TransparencyLogEntry @dataclass(frozen=True) class RekorSignature: """ Represents a `RekorSignature` containing: - the name of the signature, e.g. "rekor.sigstage.dev" - the signature hash - the base64 signature """ name: str sig_hash: bytes signature: bytes class LogCheckpoint(BaseModel): """ Represents a Rekor `LogCheckpoint` containing: - an origin, e.g. "rekor.sigstage.dev - 8050909264565447525" - the size of the log, - the hash of the log, - and any optional ancillary constants, e.g. "Timestamp: 1679349379012118479" See: """ origin: StrictStr log_size: int log_hash: StrictStr other_content: list[str] @classmethod def from_text(cls, text: str) -> LogCheckpoint: """ Serialize from the text header ("note") of a SignedNote. """ lines = text.strip().split("\n") if len(lines) < 3: raise VerificationError("malformed LogCheckpoint: too few items in header") origin = lines[0] if len(origin) == 0: raise VerificationError("malformed LogCheckpoint: empty origin") log_size = int(lines[1]) root_hash = base64.b64decode(lines[2]).hex() return LogCheckpoint( origin=origin, log_size=log_size, log_hash=root_hash, other_content=lines[3:], ) @classmethod def to_text(self) -> str: """ Serialize a `LogCheckpoint` into text format. See class definition for a prose description of the format. """ return "\n".join( [self.origin, str(self.log_size), self.log_hash, *self.other_content] ) @dataclass(frozen=True) class SignedNote: """ Represents a "signed note" containing a note and its corresponding list of signatures. """ note: StrictStr = Field(..., alias="note") signatures: list[RekorSignature] = Field(..., alias="signatures") @classmethod def from_text(cls, text: str) -> SignedNote: """ Deserialize from a bundled text 'note'. A note contains: - a name, a string associated with the signer, - a separator blank line, - and signature(s), each signature takes the form `\u2014 NAME SIGNATURE\n` (where \u2014 == em dash). This is derived from Rekor's `UnmarshalText`: """ separator: str = "\n\n" if text.count(separator) != 1: raise VerificationError( "note must contain one blank line, delineating the text from the signature block" ) split = text.index(separator) header: str = text[: split + 1] data: str = text[split + len(separator) :] if len(data) == 0: raise VerificationError( "malformed Note: must contain at least one signature" ) if data[-1] != "\n": raise VerificationError( "malformed Note: data section must end with newline" ) sig_parser = re.compile(r"\u2014 (\S+) (\S+)\n") signatures: list[RekorSignature] = [] for name, signature in re.findall(sig_parser, data): signature_bytes: bytes = base64.b64decode(signature) if len(signature_bytes) < 5: raise VerificationError( "malformed Note: signature contains too few bytes" ) signature = RekorSignature( name=name, sig_hash=struct.unpack(">4s", signature_bytes[0:4])[0], signature=base64.b64encode(signature_bytes[4:]), ) signatures.append(signature) return cls(note=header, signatures=signatures) def verify(self, rekor_keyring: RekorKeyring, key_id: KeyID) -> None: """ Verify the `SignedNote` using the given RekorKeyring and KeyID. """ note = str.encode(self.note) for sig in self.signatures: if sig.sig_hash == key_id[:4]: try: rekor_keyring.verify( key_id=key_id, signature=base64.b64decode(sig.signature), data=note, ) return except VerificationError as sig_err: raise VerificationError(f"checkpoint: invalid signature: {sig_err}") raise VerificationError( f"checkpoint: Signature not found for log ID {key_id.hex()}" ) @dataclass(frozen=True) class SignedCheckpoint: """ Represents a *signed* `Checkpoint`: a `LogCheckpoint` and its corresponding `SignedNote`. """ signed_note: SignedNote checkpoint: LogCheckpoint @classmethod def from_text(cls, text: str) -> SignedCheckpoint: """ Create a new `SignedCheckpoint` from the text representation. """ signed_note = SignedNote.from_text(text) checkpoint = LogCheckpoint.from_text(signed_note.note) return cls(signed_note=signed_note, checkpoint=checkpoint) def verify_checkpoint(rekor_keyring: RekorKeyring, entry: TransparencyLogEntry) -> None: """ Verify the inclusion proof's checkpoint. """ inclusion_proof = entry._inner.inclusion_proof if inclusion_proof.checkpoint is None: raise VerificationError("Inclusion proof does not contain a checkpoint") # verification occurs in two stages: # 1) verify the signature on the checkpoint # 2) verify the root hash in the checkpoint matches the root hash from the inclusion proof. signed_checkpoint = SignedCheckpoint.from_text(inclusion_proof.checkpoint.envelope) signed_checkpoint.signed_note.verify( rekor_keyring, KeyID(entry._inner.log_id.key_id), ) checkpoint_hash = signed_checkpoint.checkpoint.log_hash root_hash = inclusion_proof.root_hash.hex() if checkpoint_hash != root_hash: raise VerificationError( "Inclusion proof contains invalid root hash signature: ", f"expected {checkpoint_hash} got {root_hash}", ) ================================================ FILE: sigstore/_internal/rekor/client.py ================================================ # 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. """ Client implementation for interacting with Rekor (v1). """ from __future__ import annotations import base64 import json import logging import threading from abc import ABC from dataclasses import dataclass from typing import Any import rekor_types import requests from cryptography.hazmat.primitives import serialization from cryptography.x509 import Certificate from sigstore._internal import USER_AGENT from sigstore._internal.rekor import ( EntryRequestBody, RekorClientError, RekorLogSubmitter, ) from sigstore.dsse import Envelope from sigstore.hashes import Hashed from sigstore.models import TransparencyLogEntry _logger = logging.getLogger(__name__) DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" STAGING_REKOR_URL = "https://rekor.sigstage.dev" @dataclass(frozen=True) class RekorLogInfo: """ Represents information about the Rekor log. """ root_hash: str tree_size: int signed_tree_head: str tree_id: str raw_data: dict[str, Any] @classmethod def from_response(cls, dict_: dict[str, Any]) -> RekorLogInfo: """ Create a new `RekorLogInfo` from the given API response. """ return cls( root_hash=dict_["rootHash"], tree_size=dict_["treeSize"], signed_tree_head=dict_["signedTreeHead"], tree_id=dict_["treeID"], raw_data=dict_, ) class _Endpoint(ABC): def __init__(self, url: str, session: requests.Session) -> None: # Note that _Endpoint may not be thread safe if the same Session is provided # to an _Endpoint in multiple threads self.url = url self.session = session class RekorLog(_Endpoint): """ Represents a Rekor instance's log endpoint. """ def get(self) -> RekorLogInfo: """ Returns information about the Rekor instance's log. """ resp: requests.Response = self.session.get(self.url) try: resp.raise_for_status() except requests.HTTPError as http_error: raise RekorClientError(http_error) return RekorLogInfo.from_response(resp.json()) @property def entries(self) -> RekorEntries: """ Returns a `RekorEntries` capable of accessing detailed information about individual log entries. """ return RekorEntries(f"{self.url}/entries", session=self.session) class RekorEntries(_Endpoint): """ Represents the individual log entry endpoints on a Rekor instance. """ def get( self, *, uuid: str | None = None, log_index: int | None = None ) -> TransparencyLogEntry: """ Retrieve a specific log entry, either by UUID or by log index. Either `uuid` or `log_index` must be present, but not both. """ if not (bool(uuid) ^ bool(log_index)): raise ValueError("uuid or log_index required, but not both") resp: requests.Response if uuid is not None: resp = self.session.get(f"{self.url}/{uuid}") else: resp = self.session.get(self.url, params={"logIndex": log_index}) try: resp.raise_for_status() except requests.HTTPError as http_error: raise RekorClientError(http_error) return TransparencyLogEntry._from_v1_response(resp.json()) def post( self, payload: EntryRequestBody, ) -> TransparencyLogEntry: """ Submit a new entry for inclusion in the Rekor log. """ _logger.debug(f"proposed: {json.dumps(payload)}") resp: requests.Response = self.session.post(self.url, json=payload) try: resp.raise_for_status() except requests.HTTPError as http_error: raise RekorClientError(http_error) integrated_entry = resp.json() _logger.debug(f"integrated: {integrated_entry}") return TransparencyLogEntry._from_v1_response(integrated_entry) @property def retrieve(self) -> RekorEntriesRetrieve: """ Returns a `RekorEntriesRetrieve` capable of retrieving entries. """ return RekorEntriesRetrieve(f"{self.url}/retrieve/", session=self.session) class RekorEntriesRetrieve(_Endpoint): """ Represents the entry retrieval endpoints on a Rekor instance. """ def post( self, expected_entry: rekor_types.Hashedrekord | rekor_types.Dsse, ) -> TransparencyLogEntry | None: """ Retrieves an extant Rekor entry, identified by its artifact signature, artifact hash, and signing certificate. Returns None if Rekor has no entry corresponding to the signing materials. """ data = {"entries": [expected_entry.model_dump(mode="json", by_alias=True)]} resp: requests.Response = self.session.post(self.url, json=data) try: resp.raise_for_status() except requests.HTTPError as http_error: if http_error.response and http_error.response.status_code == 404: return None raise RekorClientError(http_error) results = resp.json() # The response is a list of `{uuid: LogEntry}` objects. # We select the oldest entry for our actual return value, # since a malicious actor could conceivably spam the log with # newer duplicate entries. oldest_entry: TransparencyLogEntry | None = None for result in results: entry = TransparencyLogEntry._from_v1_response(result) # We expect every entry in Rekor v1 to have an integrated time. if entry._inner.integrated_time is None: raise ValueError( f"Rekor v1 gave us an entry without an integrated time: {entry._inner.log_index}" ) if ( oldest_entry is None or entry._inner.integrated_time < oldest_entry._inner.integrated_time # type: ignore[operator] ): oldest_entry = entry return oldest_entry class RekorClient(RekorLogSubmitter): """The internal Rekor client""" def __init__(self, url: str) -> None: """ Create a new `RekorClient` from the given URL. """ self.url = f"{url}/api/v1" self._thread_local = threading.local() @classmethod def production(cls) -> RekorClient: """ Returns a `RekorClient` populated with the default Rekor production instance. """ return cls( DEFAULT_REKOR_URL, ) @classmethod def staging(cls) -> RekorClient: """ Returns a `RekorClient` populated with the default Rekor staging instance. """ return cls(STAGING_REKOR_URL) @property def _session(self) -> requests.Session: """ Lazy-initialized thread-local session object """ if not hasattr(self._thread_local, "session"): session = requests.Session() session.headers.update( { "Content-Type": "application/json", "Accept": "application/json", "User-Agent": USER_AGENT, } ) self._thread_local.session = session return self._thread_local.session # type: ignore[no-any-return] @property def log(self) -> RekorLog: """ Returns a `RekorLog` adapter for making requests to a Rekor log. """ return RekorLog(f"{self.url}/log", session=self._session) def create_entry(self, request: EntryRequestBody) -> TransparencyLogEntry: """ Submit the request to Rekor. create_entry() can be called from multiple threads. """ return self.log.entries.post(request) def _build_hashed_rekord_request( # type: ignore[override] self, hashed_input: Hashed, signature: bytes, certificate: Certificate ) -> EntryRequestBody: """ Construct a hashed rekord payload to submit to Rekor. """ rekord = rekor_types.Hashedrekord( spec=rekor_types.hashedrekord.HashedrekordV001Schema( signature=rekor_types.hashedrekord.Signature( content=base64.b64encode(signature).decode(), public_key=rekor_types.hashedrekord.PublicKey( content=base64.b64encode( certificate.public_bytes( encoding=serialization.Encoding.PEM ) ).decode() ), ), data=rekor_types.hashedrekord.Data( hash=rekor_types.hashedrekord.Hash( algorithm=hashed_input._as_hashedrekord_algorithm(), value=hashed_input.digest.hex(), ) ), ), ) return EntryRequestBody(rekord.model_dump(mode="json", by_alias=True)) def _build_dsse_request( # type: ignore[override] self, envelope: Envelope, certificate: Certificate ) -> EntryRequestBody: """ Construct a dsse request to submit to Rekor. """ dsse = rekor_types.Dsse( spec=rekor_types.dsse.DsseSchema( # NOTE: mypy can't see that this kwarg is correct due to two interacting # behaviors/bugs (one pydantic, one datamodel-codegen): # See: # See: proposed_content=rekor_types.dsse.ProposedContent( # type: ignore[call-arg] envelope=envelope.to_json(), verifiers=[ base64.b64encode( certificate.public_bytes( encoding=serialization.Encoding.PEM ) ).decode() ], ), ), ) return EntryRequestBody(dsse.model_dump(mode="json", by_alias=True)) ================================================ FILE: sigstore/_internal/rekor/client_v2.py ================================================ # Copyright 2025 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. """ Client implementation for interacting with Rekor v2. """ from __future__ import annotations import base64 import json import logging import threading import requests from cryptography.hazmat.primitives import serialization from cryptography.x509 import Certificate from sigstore_models.common import v1 as common_v1 from sigstore_models.rekor import v2 as rekor_v2 from sigstore_models.rekor.v1 import TransparencyLogEntry as _TransparencyLogEntry from sigstore._internal import USER_AGENT from sigstore._internal.key_details import _get_key_details from sigstore._internal.rekor import ( EntryRequestBody, RekorClientError, RekorLogSubmitter, ) from sigstore.dsse import Envelope from sigstore.hashes import Hashed from sigstore.models import TransparencyLogEntry _logger = logging.getLogger(__name__) class RekorV2Client(RekorLogSubmitter): """ The internal Rekor client for the v2 API. See https://github.com/sigstore/rekor-tiles/blob/main/CLIENTS.md """ def __init__(self, base_url: str) -> None: """ Create a new `RekorV2Client` from the given URL. """ self.url = f"{base_url}/api/v2" self._thread_local = threading.local() @property def _session(self) -> requests.Session: """ Lazy-initialized thread-local session object """ if not hasattr(self._thread_local, "session"): session = requests.Session() session.headers.update( { "Content-Type": "application/json", "Accept": "application/json", "User-Agent": USER_AGENT, } ) self._thread_local.session = session return self._thread_local.session # type: ignore[no-any-return] def create_entry(self, payload: EntryRequestBody) -> TransparencyLogEntry: """ Submit a new entry for inclusion in the Rekor log. Note that this call can take a fairly long time as the log only responds after the entry has been included in the log. https://github.com/sigstore/rekor-tiles/blob/main/CLIENTS.md#handling-longer-requests create_entry() can be called from multiple threads. """ _logger.debug(f"proposed: {json.dumps(payload)}") resp = self._session.post( f"{self.url}/log/entries", json=payload, ) try: resp.raise_for_status() except requests.HTTPError as http_error: raise RekorClientError(http_error) integrated_entry = resp.json() _logger.debug(f"integrated: {integrated_entry}") inner = _TransparencyLogEntry.from_dict(integrated_entry) return TransparencyLogEntry(inner) @classmethod def _build_hashed_rekord_request( cls, hashed_input: Hashed, signature: bytes, certificate: Certificate, ) -> EntryRequestBody: """ Construct a hashed rekord request to submit to Rekor. """ req = rekor_v2.entry.CreateEntryRequest( hashed_rekord_request_v002=rekor_v2.hashedrekord.HashedRekordRequestV002( digest=base64.b64encode(hashed_input.digest), signature=rekor_v2.verifier.Signature( content=base64.b64encode(signature), verifier=rekor_v2.verifier.Verifier( x509_certificate=common_v1.X509Certificate( raw_bytes=base64.b64encode( certificate.public_bytes( encoding=serialization.Encoding.DER ) ) ), key_details=_get_key_details(certificate), ), ), ) ) return EntryRequestBody(req.to_dict()) @classmethod def _build_dsse_request( cls, envelope: Envelope, certificate: Certificate ) -> EntryRequestBody: """ Construct a dsse request to submit to Rekor. """ req = rekor_v2.entry.CreateEntryRequest( dsse_request_v002=rekor_v2.dsse.DSSERequestV002( envelope=envelope._inner, verifiers=[ rekor_v2.verifier.Verifier( x509_certificate=common_v1.X509Certificate( raw_bytes=base64.b64encode( certificate.public_bytes( encoding=serialization.Encoding.DER ) ) ), key_details=_get_key_details(certificate), ) ], ) ) return EntryRequestBody(req.to_dict()) ================================================ FILE: sigstore/_internal/sct.py ================================================ # 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. """ Utilities for verifying signed certificate timestamps. """ import logging import struct from datetime import timezone from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.x509 import ( Certificate, ExtendedKeyUsage, ExtensionNotFound, PrecertificateSignedCertificateTimestamps, ) from cryptography.x509.certificate_transparency import ( LogEntryType, SignedCertificateTimestamp, ) from cryptography.x509.oid import ExtendedKeyUsageOID from sigstore._internal.trust import CTKeyring from sigstore._utils import ( KeyID, cert_is_ca, key_id, ) from sigstore.errors import VerificationError _logger = logging.getLogger(__name__) def _pack_signed_entry( sct: SignedCertificateTimestamp, cert: Certificate, issuer_key_id: bytes | None ) -> bytes: fields = [] if sct.entry_type == LogEntryType.X509_CERTIFICATE: # When dealing with a "normal" certificate, our signed entry looks like this: # # [0]: opaque ASN.1Cert<1..2^24-1> pack_format = "!BBB{cert_der_len}s" cert_der = cert.public_bytes(encoding=serialization.Encoding.DER) elif sct.entry_type == LogEntryType.PRE_CERTIFICATE: if not issuer_key_id or len(issuer_key_id) != 32: raise VerificationError("API misuse: issuer key ID missing") # When dealing with a precertificate, our signed entry looks like this: # # [0]: issuer_key_id[32] # [1]: opaque TBSCertificate<1..2^24-1> pack_format = "!32sBBB{cert_der_len}s" # Precertificates must have their SCT list extension filtered out. cert_der = cert.tbs_precertificate_bytes fields.append(issuer_key_id) else: raise VerificationError(f"unknown SCT log entry type: {sct.entry_type!r}") # The `opaque` length is a u24, which isn't directly supported by `struct`. # So we have to decompose it into 3 bytes. unused, len1, len2, len3 = struct.unpack( "!4B", struct.pack("!I", len(cert_der)), ) if unused: raise VerificationError( f"Unexpectedly large certificate length: {len(cert_der)}" ) pack_format = pack_format.format(cert_der_len=len(cert_der)) fields.extend((len1, len2, len3, cert_der)) return struct.pack(pack_format, *fields) def _pack_digitally_signed( sct: SignedCertificateTimestamp, cert: Certificate, issuer_key_id: KeyID | None, ) -> bytes: """ Packs the contents of `cert` (and some pieces of `sct`) into a structured blob, one that forms the signature body of the "digitally-signed" struct for an SCT. The format of the digitally signed data is described in IETF's RFC 6962. """ # This constructs the "core" `signed_entry` field, which is either # the public bytes of the cert *or* the TBSPrecertificate (with some # filtering), depending on whether our SCT is for a precertificate. signed_entry = _pack_signed_entry(sct, cert, issuer_key_id) # Assemble a format string with the certificate length baked in and then pack the digitally # signed data # fmt: off pattern = f"!BBQH{len(signed_entry)}sH{len(sct.extension_bytes)}s" timestamp = sct.timestamp.replace(tzinfo=timezone.utc) data = struct.pack( pattern, sct.version.value, # sct_version 0, # signature_type (certificate_timestamp(0)) int(timestamp.timestamp() * 1000), # timestamp (milliseconds) sct.entry_type.value, # entry_type (x509_entry(0) | precert_entry(1)) signed_entry, # select(entry_type) -> signed_entry (see above) len(sct.extension_bytes), # extensions (opaque CtExtensions<0..2^16-1>) sct.extension_bytes, ) # fmt: on return data def _is_preissuer(issuer: Certificate) -> bool: try: ext_key_usage = issuer.extensions.get_extension_for_class(ExtendedKeyUsage) # If we do not have any EKU, we certainly do not have CT Ext except ExtensionNotFound: return False return ExtendedKeyUsageOID.CERTIFICATE_TRANSPARENCY in ext_key_usage.value def _get_issuer_cert(chain: list[Certificate]) -> Certificate: issuer = chain[0] if _is_preissuer(issuer): issuer = chain[1] return issuer def _get_signed_certificate_timestamp( certificate: Certificate, ) -> SignedCertificateTimestamp: """Retrieve the embedded SCT from the certificate. Raise VerificationError if certificate does not contain exactly one SCT """ try: timestamps = certificate.extensions.get_extension_for_class( PrecertificateSignedCertificateTimestamps ).value except ExtensionNotFound: raise VerificationError( "Certificate does not contain a signed certificate timestamp extension" ) if len(timestamps) != 1: raise VerificationError( f"Expected one certificate timestamp, found {len(timestamps)}" ) sct: SignedCertificateTimestamp = timestamps[0] return sct def _cert_is_ca(cert: Certificate) -> bool: _logger.debug(f"Found {cert.subject} as issuer, verifying if it is a ca") try: cert_is_ca(cert) except VerificationError as e: _logger.debug(f"Invalid {cert.subject}: failed to validate as a CA: {e}") return False return True def verify_sct( cert: Certificate, chain: list[Certificate], ct_keyring: CTKeyring, ) -> None: """ Verify a signed certificate timestamp. An SCT is verified by reconstructing its "digitally-signed" payload and verifying that the signature provided in the SCT is valid against one of the keys present in the CT keyring (i.e., the keys used by the CT log to sign SCTs). """ sct = _get_signed_certificate_timestamp(cert) issuer_key_id = None if sct.entry_type == LogEntryType.PRE_CERTIFICATE: # If we're verifying an SCT for a precertificate, we need to # find its issuer in the chain and calculate a hash over # its public key information, as part of the "binding" proof # that ties the issuer to the final certificate. issuer_cert = _get_issuer_cert(chain) issuer_pubkey = issuer_cert.public_key() if not _cert_is_ca(issuer_cert): raise VerificationError( f"SCT verify: Invalid issuer pubkey basicConstraint (not a CA): {issuer_pubkey}" ) if not isinstance(issuer_pubkey, rsa.RSAPublicKey | ec.EllipticCurvePublicKey): raise VerificationError( f"SCT verify: invalid issuer pubkey format (not ECDSA or RSA): {issuer_pubkey}" ) issuer_key_id = key_id(issuer_pubkey) digitally_signed = _pack_digitally_signed(sct, cert, issuer_key_id) if not isinstance(sct.signature_hash_algorithm, hashes.SHA256): raise VerificationError( "Found unexpected hash algorithm in SCT: only SHA256 is supported " f"(expected {hashes.SHA256}, got {sct.signature_hash_algorithm})" ) try: _logger.debug(f"attempting to verify SCT with key ID {sct.log_id.hex()}") # NOTE(ww): In terms of the DER structure, the SCT's `LogID` contains a # singular `opaque key_id[32]`. Cryptography's APIs don't bother # to expose this trivial single member, so we use the `log_id` # attribute directly. ct_keyring.verify( key_id=KeyID(sct.log_id), signature=sct.signature, data=digitally_signed ) except VerificationError as exc: raise VerificationError(f"SCT verify failed: {exc}") ================================================ FILE: sigstore/_internal/timestamp.py ================================================ # 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. """ Utilities to deal with sources of signed time. """ import enum import threading from dataclasses import dataclass from datetime import datetime import requests from rfc3161_client import ( TimestampRequestBuilder, TimeStampResponse, decode_timestamp_response, ) from rfc3161_client.base import HashAlgorithm from sigstore._internal import USER_AGENT CLIENT_TIMEOUT: int = 5 class TimestampSource(enum.Enum): """Represents the source of a timestamp.""" TIMESTAMP_AUTHORITY = enum.auto() TRANSPARENCY_SERVICE = enum.auto() @dataclass class TimestampVerificationResult: """Represents a timestamp used by the Verifier. A Timestamp either comes from a Timestamping Service (RFC3161) or the Transparency Service. """ source: TimestampSource time: datetime class TimestampError(Exception): """ A generic error in the TimestampAuthority client. """ pass class TimestampAuthorityClient: """Internal client to deal with a Timestamp Authority""" def __init__(self, url: str) -> None: """ Create a new `TimestampAuthorityClient` from the given URL. """ self.url = url self._thread_local = threading.local() @property def _session(self) -> requests.Session: """ Lazy-initialized thread-local session object """ if not hasattr(self._thread_local, "session"): session = requests.Session() session.headers.update( { "Content-Type": "application/timestamp-query", "User-Agent": USER_AGENT, } ) self._thread_local.session = session return self._thread_local.session # type: ignore[no-any-return] def request_timestamp(self, signature: bytes) -> TimeStampResponse: """ Timestamp the signature using the configured Timestamp Authority. This method generates a RFC3161 Timestamp Request and sends it to a TSA. The received response is parsed but *not* cryptographically verified. request_timestamp() can be called from multiple threads. Raises a TimestampError on failure. """ # Build the timestamp request try: timestamp_request = ( TimestampRequestBuilder() .hash_algorithm(HashAlgorithm.SHA256) .data(signature) .nonce(nonce=True) .build() ) except ValueError as error: msg = f"invalid request: {error}" raise TimestampError(msg) # Send it to the TSA for signing try: response = self._session.post( self.url, data=timestamp_request.as_bytes(), timeout=CLIENT_TIMEOUT, ) response.raise_for_status() except requests.RequestException as error: msg = f"error while sending the request to the TSA: {error}" raise TimestampError(msg) # Check that we can parse the response but do not *verify* it try: timestamp_response = decode_timestamp_response(response.content) except ValueError as e: msg = f"invalid response: {e}" raise TimestampError(msg) return timestamp_response ================================================ FILE: sigstore/_internal/trust.py ================================================ # Copyright 2023 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. """ Client trust configuration and trust root management for sigstore-python. """ from __future__ import annotations import logging from dataclasses import dataclass from datetime import datetime from enum import Enum from pathlib import Path from typing import ClassVar, NewType import cryptography.hazmat.primitives.asymmetric.padding as padding from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa from cryptography.x509 import ( Certificate, load_der_x509_certificate, ) from sigstore_models.common import v1 as common_v1 from sigstore_models.trustroot import v1 as trustroot_v1 from sigstore._utils import ( KeyID, PublicKey, is_timerange_valid, key_id, load_der_public_key, ) from sigstore.errors import Error, VerificationError # Versions supported by this client REKOR_VERSIONS = [1, 2] TSA_VERSIONS = [1] FULCIO_VERSIONS = [1] OIDC_VERSIONS = [1] _logger = logging.getLogger(__name__) @dataclass(init=False) class Key: """ Represents a key in a `Keyring`. """ hash_algorithm: hashes.HashAlgorithm | None key: PublicKey key_id: KeyID _RSA_SHA_256_DETAILS: ClassVar = { common_v1.PublicKeyDetails.PKCS1_RSA_PKCS1V5, common_v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256, common_v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256, common_v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256, } _EC_DETAILS_TO_HASH: ClassVar = { common_v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256: hashes.SHA256(), common_v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384: hashes.SHA384(), common_v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512: hashes.SHA512(), } def __init__(self, public_key: common_v1.PublicKey) -> None: """ Construct a key from the given Sigstore PublicKey message. """ # NOTE: `raw_bytes` is marked as `optional` in the `PublicKey` message, # for unclear reasons. if not public_key.raw_bytes: raise VerificationError("public key is empty") hash_algorithm: hashes.HashAlgorithm | None if public_key.key_details in self._RSA_SHA_256_DETAILS: hash_algorithm = hashes.SHA256() key = load_der_public_key(public_key.raw_bytes, types=(rsa.RSAPublicKey,)) elif public_key.key_details in self._EC_DETAILS_TO_HASH: hash_algorithm = self._EC_DETAILS_TO_HASH[public_key.key_details] key = load_der_public_key( public_key.raw_bytes, types=(ec.EllipticCurvePublicKey,) ) elif public_key.key_details == common_v1.PublicKeyDetails.PKIX_ED25519: hash_algorithm = None key = load_der_public_key( public_key.raw_bytes, types=(ed25519.Ed25519PublicKey,) ) else: raise VerificationError(f"unsupported key type: {public_key.key_details}") self.hash_algorithm = hash_algorithm self.key = key self.key_id = key_id(key) def verify(self, signature: bytes, data: bytes) -> None: """ Verifies the given `data` against `signature` using the current key. """ if isinstance(self.key, rsa.RSAPublicKey) and self.hash_algorithm is not None: self.key.verify( signature=signature, data=data, # TODO: Parametrize this as well, for PSS. padding=padding.PKCS1v15(), algorithm=self.hash_algorithm, ) elif ( isinstance(self.key, ec.EllipticCurvePublicKey) and self.hash_algorithm is not None ): self.key.verify( signature=signature, data=data, signature_algorithm=ec.ECDSA(self.hash_algorithm), ) elif ( isinstance(self.key, ed25519.Ed25519PublicKey) and self.hash_algorithm is None ): self.key.verify( signature=signature, data=data, ) else: # Unreachable without API misuse. raise VerificationError(f"keyring: unsupported key: {self.key}") class Keyring: """ Represents a set of keys, each of which is a potentially valid verifier. """ def __init__(self, public_keys: list[common_v1.PublicKey] = []): """ Create a new `Keyring`, with `keys` as the initial set of verifying keys. """ self._keyring: dict[KeyID, Key] = {} for public_key in public_keys: try: key = Key(public_key) self._keyring[key.key_id] = key except VerificationError as e: _logger.warning(f"Failed to load a trusted root key: {e}") def verify(self, *, key_id: KeyID, signature: bytes, data: bytes) -> None: """ Verify that `signature` is a valid signature for `data`, using the key identified by `key_id`. `key_id` is an unauthenticated hint; if no key matches the given key ID, all keys in the keyring are tried. Raises if the signature is invalid, i.e. is not valid for any of the keys in the keyring. """ key = self._keyring.get(key_id) candidates = [key] if key is not None else list(self._keyring.values()) # Try to verify each candidate key. In the happy case, this will # be exactly one candidate. valid = False for candidate in candidates: try: candidate.verify(signature, data) valid = True break except InvalidSignature: pass if not valid: raise VerificationError("keyring: invalid signature") RekorKeyring = NewType("RekorKeyring", Keyring) CTKeyring = NewType("CTKeyring", Keyring) class KeyringPurpose(str, Enum): """ Keyring purpose typing """ SIGN = "sign" VERIFY = "verify" def __str__(self) -> str: """Returns the purpose string value.""" return self.value class CertificateAuthority: """ Certificate Authority used in a Trusted Root configuration. """ def __init__(self, inner: trustroot_v1.CertificateAuthority): """ Construct a new `CertificateAuthority`. @api private """ self._inner = inner self._certificates: list[Certificate] = [] self._verify() @classmethod def from_json(cls, path: str) -> CertificateAuthority: """ Create a CertificateAuthority directly from JSON. """ inner = trustroot_v1.CertificateAuthority.from_json(Path(path).read_bytes()) return cls(inner) def _verify(self) -> None: """ Verify and load the certificate authority. """ self._certificates = [ load_der_x509_certificate(cert.raw_bytes) for cert in self._inner.cert_chain.certificates ] if not self._certificates: raise Error("missing a certificate in Certificate Authority") @property def validity_period_start(self) -> datetime: """ Validity period start. """ return self._inner.valid_for.start @property def validity_period_end(self) -> datetime | None: """ Validity period end. """ return self._inner.valid_for.end def certificates(self, *, allow_expired: bool) -> list[Certificate]: """ Return a list of certificates in the authority chain. The certificates are returned in order from leaf to root, with any intermediate certificates in between. """ if not is_timerange_valid(self._inner.valid_for, allow_expired=allow_expired): return [] return self._certificates ================================================ FILE: sigstore/_internal/tuf.py ================================================ # 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. """ TUF functionality for `sigstore-python`. """ from __future__ import annotations import logging from functools import lru_cache from pathlib import Path from urllib import parse import platformdirs from tuf.api import exceptions as TUFExceptions from tuf.ngclient import Updater, UpdaterConfig # type: ignore[attr-defined] from sigstore import __version__ from sigstore._utils import read_embedded from sigstore.errors import TUFError _logger = logging.getLogger(__name__) DEFAULT_TUF_URL = "https://tuf-repo-cdn.sigstore.dev" STAGING_TUF_URL = "https://tuf-repo-cdn.sigstage.dev" def _get_dirs(url: str) -> tuple[Path, Path]: """ Given a TUF repository URL, return suitable local metadata and cache directories. These directories are not guaranteed to already exist. """ app_name = "sigstore-python" app_author = "sigstore" repo_base = parse.quote(url, safe="") tuf_data_dir = Path(platformdirs.user_data_dir(app_name, app_author)) / "tuf" tuf_cache_dir = Path(platformdirs.user_cache_dir(app_name, app_author)) / "tuf" return (tuf_data_dir / repo_base), (tuf_cache_dir / repo_base) class TrustUpdater: """Internal trust root (certificates and keys) downloader. TrustUpdater discovers the currently valid certificates and keys and securely downloads them from the remote TUF repository at 'url'. TrustUpdater expects to find an initial root.json in either the local metadata directory for this URL, or (as special case for the sigstore.dev production and staging instances) in the application resources. """ def __init__( self, url: str, offline: bool = False, bootstrap_root: Path | None = None ) -> None: """ Create a new `TrustUpdater`, pulling from the given `url`. TrustUpdater expects that either embedded data contains a root.json for this url or that `bootstrap_root` is provided as argument. If not `offline`, TrustUpdater will update the TUF metadata from the remote repository. """ # not canonicalization, just handling trailing slash as common mistake: url = url.rstrip("/") self._metadata_dir, self._targets_dir = _get_dirs(url) # Populate targets cache so we don't have to download these versions self._targets_dir.mkdir(parents=True, exist_ok=True) for artifact in ["trusted_root.json", "signing_config.v0.2.json"]: artifact_path = self._targets_dir / artifact if not artifact_path.exists(): try: data = read_embedded(artifact, url) artifact_path.write_bytes(data) except FileNotFoundError: pass # this is ok: we only have embedded data for specific repos _logger.debug(f"TUF metadata: {self._metadata_dir}") _logger.debug(f"TUF targets cache: {self._targets_dir}") self._updater: Updater | None = None if offline: _logger.warning( "TUF repository is loaded in offline mode; updates will not be performed" ) else: # Initialize and update the toplevel TUF metadata try: root_json: bytes | None = read_embedded("root.json", url) except FileNotFoundError: # We do not have embedded root metadata for this URL: we can still # initialize _if_ given bootstrap root (i.e. during "sigstore trust-instance") # or local metadata exists already (after "sigstore trust-instance") root_json = bootstrap_root.read_bytes() if bootstrap_root else None try: self._updater = Updater( metadata_dir=str(self._metadata_dir), metadata_base_url=url, target_base_url=parse.urljoin(f"{url}/", "targets/"), target_dir=str(self._targets_dir), config=UpdaterConfig( app_user_agent=f"sigstore-python/{__version__}" ), bootstrap=root_json, ) self._updater.refresh() except Exception as e: raise TUFError("Failed to refresh TUF metadata") from e @lru_cache() def get_trusted_root_path(self) -> str: """Return local path to currently valid trusted root file""" if not self._updater: _logger.debug("Using unverified trusted root from cache") return str(self._targets_dir / "trusted_root.json") root_info = self._updater.get_targetinfo("trusted_root.json") if root_info is None: raise TUFError("Unsupported TUF configuration: no trusted root") path = self._updater.find_cached_target(root_info) if path is None: try: path = self._updater.download_target(root_info) except ( TUFExceptions.DownloadError, TUFExceptions.RepositoryError, ) as e: raise TUFError("Failed to download trusted key bundle") from e _logger.debug("Found and verified trusted root") return path @lru_cache() def get_signing_config_path(self) -> str: """Return local path to currently valid signing config file""" if not self._updater: _logger.debug("Using unverified signing config from cache") return str(self._targets_dir / "signing_config.v0.2.json") root_info = self._updater.get_targetinfo("signing_config.v0.2.json") if root_info is None: raise TUFError("Unsupported TUF configuration: no signing config") path = self._updater.find_cached_target(root_info) if path is None: try: path = self._updater.download_target(root_info) except ( TUFExceptions.DownloadError, TUFExceptions.RepositoryError, ) as e: raise TUFError("Failed to download signing config") from e _logger.debug("Found and verified signing config") return path ================================================ FILE: sigstore/_store/__init__.py ================================================ # 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. """ An empty module, used to assist Python's resource machinery in embedding assets. """ # NOTE: This is arguably incorrect, since _store only contains non-Python files. # However, due to how `importlib.resources` is designed, only top-level resources # inside of packages or modules can be accessed, so this directory needs to be a # module in order for us to programmatically access the keys and root certs in it. # # Why do we bother with `importlib` at all? Because we might be installed as a # ZIP file or an Egg, which in turn means that our resource files don't actually # exist separately on disk. `importlib` is the only reliable way to access them. ================================================ FILE: sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/root.json ================================================ { "signatures": [ { "keyid": "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81", "sig": "3045022100d404545c87d31829c26820dc963389ef8497dbb1a712e08f5e81ce5a92c3ec600220314d108bc9e827c1a67610d1c90d5fb9a426ccc8bde009fb663c292b1728e6a4" }, { "keyid": "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc", "sig": "304402204cbe823ca173f04c4fd59cb01941efbd9f2b9452f405a3cd1c5bcb7481a818f902201cb4223b74b8e54f5de44936ae3c7adef32959da8d7d9625d23e464263c39e97" }, { "keyid": "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237", "sig": "" }, { "keyid": "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5", "sig": "" } ], "signed": { "_type": "root", "consistent_snapshot": true, "expires": "2026-05-22T19:23:14Z", "keys": { "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoxkvDOmtGEknB3M+ZkPts8joDM0X\nIH5JZwPlgC2CXs/eqOuNF8AcEWwGYRiDhV/IMlQw5bg8PLICQcgsbrDiKg==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@mnm678" }, "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE++Wv+DcLRk+mfkmlpCwl1GUi9EMh\npBUTz8K0fH7bE4mQuViGSyWA/eyMc0HvzZi6Xr0diHw0/lUPBvok214YQw==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@kommendorkapten" }, "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFHDb85JH+JYR1LQmxiz4UMokVMnP\nxKoWpaEnFCKXH8W4Fc/DfIxMnkpjCuvWUBdJXkO0aDIxwsij8TOFh2R7dw==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@joshuagl" }, "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEohqIdE+yTl4OxpX8ZxNUPrg3SL9H\nBDnhZuceKkxy2oMhUOxhWweZeG3bfM1T4ZLnJimC6CAYVU5+F5jZCoftRw==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@jku" }, "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExxmEtmhF5U+i+v/6he4BcSLzCgMx\n/0qSrvDg6bUWwUrkSKS2vDpcJrhGy5fmmhRrGawjPp1ALpC3y1kqFTpXDg==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-online-uri": "gcpkms:projects/projectsigstore-staging/locations/global/keyRings/tuf-keyring/cryptoKeys/tuf-key/cryptoKeyVersions/2" } }, "roles": { "root": { "keyids": [ "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81", "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc", "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237", "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5" ], "threshold": 2 }, "snapshot": { "keyids": [ "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4" ], "threshold": 1, "x-tuf-on-ci-expiry-period": 3650, "x-tuf-on-ci-signing-period": 365 }, "targets": { "keyids": [ "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81", "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc", "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237", "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5" ], "threshold": 1 }, "timestamp": { "keyids": [ "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4" ], "threshold": 1, "x-tuf-on-ci-expiry-period": 7, "x-tuf-on-ci-signing-period": 6 } }, "spec_version": "1.0", "version": 13, "x-tuf-on-ci-expiry-period": 182, "x-tuf-on-ci-signing-period": 35 } } ================================================ FILE: sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/signing_config.v0.2.json ================================================ { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.sigstage.dev", "majorApiVersion": 1, "validFor": { "start": "2022-04-14T21:38:40Z" }, "operator": "sigstore.dev" } ], "oidcUrls": [ { "url": "https://oauth2.sigstage.dev/auth", "majorApiVersion": 1, "validFor": { "start": "2025-04-16T00:00:00Z" }, "operator": "sigstore.dev" } ], "rekorTlogUrls": [ { "url": "https://log2025-alpha3.rekor.sigstage.dev", "majorApiVersion": 2, "validFor": { "start": "2025-09-22T11:00:00Z" }, "operator": "sigstore.dev" }, { "url": "https://log2025-alpha2.rekor.sigstage.dev", "majorApiVersion": 2, "validFor": { "start": "2025-08-20T07:24:08Z", "end": "2025-09-22T11:00:00Z" }, "operator": "sigstore.dev" }, { "url": "https://log2025-alpha1.rekor.sigstage.dev", "majorApiVersion": 2, "validFor": { "start": "2025-05-07T12:00:00Z", "end": "2025-08-20T07:24:08Z" }, "operator": "sigstore.dev" }, { "url": "https://rekor.sigstage.dev", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27Z" }, "operator": "sigstore.dev" } ], "tsaUrls": [ { "url": "https://timestamp.sigstage.dev/api/v1/timestamp", "majorApiVersion": 1, "validFor": { "start": "2025-04-09T00:00:00Z" }, "operator": "sigstore.dev" } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } ================================================ FILE: sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/trusted_root.json ================================================ { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "https://rekor.sigstage.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27Z" } }, "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" } }, { "baseUrl": "https://log2025-alpha1.rekor.sigstage.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MCowBQYDK2VwAyEAPn+AREHoBaZ7wgS1zBqpxmLSGnyhxXj4lFxSdWVB8o8=", "keyDetails": "PKIX_ED25519", "validFor": { "start": "2025-04-16T00:00:00Z", "end": "2025-09-04T00:00:00Z" } }, "logId": { "keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck=" } }, { "baseUrl": "https://log2025-alpha2.rekor.sigstage.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MCowBQYDK2VwAyEAkrA8Ou2FtN7kYXCP/lpvF8vQrvh4nj+91+PWOGGzfGc=", "keyDetails": "PKIX_ED25519", "validFor": { "start": "2025-08-08T00:00:00Z", "end": "2025-09-28T00:00:00Z" } }, "logId": { "keyId": "KfSiSX2iRLyhK62SUVL47vVcqqRx/RAewpKJm8IdZTo=" } }, { "baseUrl": "https://log2025-alpha3.rekor.sigstage.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MCowBQYDK2VwAyEAlD3dVc8yaP25mPtT/sJ59D3LLxGBgW/qYrM6x6KmOqk=", "keyDetails": "PKIX_ED25519", "validFor": { "start": "2025-09-22T00:00:00Z" } }, "logId": { "keyId": "09OnDKEw7/hpZiYVPoTRzRbglHk0sylsUovegnRUlJY=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstage.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" }, { "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" } ] }, "validFor": { "start": "2022-04-14T21:38:40Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstage.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", "keyDetails": "PKCS1_RSA_PKCS1V5", "validFor": { "start": "2021-03-14T00:00:00Z", "end": "2022-07-31T00:00:00Z" } }, "logId": { "keyId": "G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=" } }, { "baseUrl": "https://ctfe.sigstage.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-07-01T00:00:00Z", "end": "2022-07-31T00:00:00Z" } }, "logId": { "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" } }, { "baseUrl": "https://ctfe.sigstage.dev/2022-2", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-07-01T00:00:00Z" } }, "logId": { "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" } } ], "timestampAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore-tsa-selfsigned" }, "uri": "https://timestamp.sigstage.dev/api/v1/timestamp", "certChain": { "certificates": [ { "rawBytes": "MIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUCPExEFKiQh0dP4sp5ltmSYSSkFUwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATt0tIDWyo4ARfL9BaSo0W5bJQEbKJTU/u7llvdjSI5aTkOAJa8tixn2+LEfPG4dMFdsMPtsIuU1qn2OqFiuMk6vHv/c+az25RQVY1oo50iMb0jIL3N4FgwhPFpZnCbQPOjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAKBggqhkjOPQQDAwNpADBmAjEA2MI1VXgbf3dUOSc95hSRypBKOab18eh2xzQtxUsHvWeY+1iFgyMluUuNR6taoSmFAjEA31m2czguZhKYX+4JSKu5pRYhBTXAd8KKQ3xdPRX/qCaLvT2qJAEQ1YQM3EJRrtI7" } ] }, "validFor": { "start": "2025-04-09T00:00:00Z" } } ] } ================================================ FILE: sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json ================================================ { "signatures": [ { "keyid": "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2", "sig": "3046022100e04c9706299be5d8c2b14fb50bcd5b9c241f10597153dfe22f943efe896b5150022100cfd7b9f06a5900784e312d02b8e336edbb3b2fab61ac14550b3112b4f9e33df4" }, { "keyid": "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06", "sig": "" }, { "keyid": "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222", "sig": "3045022100cc308ae7d390fa782ee3376ddfaa929835016e86dad81f69e2de7ec1e174432e02205fb19906a31cce146c29624443c0d0c2f33ee80dac39d72114f939607cc22937" }, { "keyid": "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70", "sig": "304502203f8aff7a30e05a8c3d904b671ab1a6e4e8a6f508b7cfa0c780e72976bee7a227022100f64c9b765526f34d9ea16339cf238893e1c3368b4f0910a61a1af27dda01ebb9" }, { "keyid": "183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632", "sig": "304502202363ca249aefa6d5f61c408a32cdd079b034a7888ddf2136dc4515ed4a728418022100b04eca42bc510ccbbf5d30783aaa936b1f137ca7a017ee9d90d3710432da0427" } ], "signed": { "_type": "root", "consistent_snapshot": true, "expires": "2026-06-22T13:27:01Z", "keys": { "0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5": { "keyid_hash_algorithms": [ "sha256", "sha512" ], "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-online-uri": "gcpkms:projects/sigstore-root-signing/locations/global/keyRings/root/cryptoKeys/timestamp/cryptoKeyVersions/1" }, "183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMxpPOJCIZ5otG4106fGJseEQi3V9\npkMYQ4uyV9Tj1M7WHXIyLG+jkfvuG0glQ1JZbRZZBV3gAR4sojdGHISeow==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@lance" }, "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06": { "keyid_hash_algorithms": [ "sha256", "sha512" ], "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@santiagotorres" }, "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222": { "keyid_hash_algorithms": [ "sha256", "sha512" ], "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@bobcallaway" }, "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70": { "keyid_hash_algorithms": [ "sha256", "sha512" ], "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@joshuagl" }, "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2": { "keyid_hash_algorithms": [ "sha256", "sha512" ], "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-keyowner": "@mnm678" } }, "roles": { "root": { "keyids": [ "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2", "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06", "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222", "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70", "183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632" ], "threshold": 3 }, "snapshot": { "keyids": [ "0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5" ], "threshold": 1, "x-tuf-on-ci-expiry-period": 3650, "x-tuf-on-ci-signing-period": 365 }, "targets": { "keyids": [ "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2", "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06", "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222", "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70", "183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632" ], "threshold": 3 }, "timestamp": { "keyids": [ "0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5" ], "threshold": 1, "x-tuf-on-ci-expiry-period": 7, "x-tuf-on-ci-signing-period": 6 } }, "spec_version": "1.0", "version": 14, "x-tuf-on-ci-expiry-period": 197, "x-tuf-on-ci-signing-period": 46 } } ================================================ FILE: sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/signing_config.v0.2.json ================================================ { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.sigstore.dev", "majorApiVersion": 1, "validFor": { "start": "2022-04-13T20:06:15.000Z" }, "operator": "sigstore.dev" } ], "oidcUrls": [ { "url": "https://oauth2.sigstore.dev/auth", "majorApiVersion": 1, "validFor": { "start": "2022-04-13T20:06:15.000Z" }, "operator": "sigstore.dev" } ], "rekorTlogUrls": [ { "url": "https://rekor.sigstore.dev", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27.000Z" }, "operator": "sigstore.dev" } ], "tsaUrls": [ { "url": "https://timestamp.sigstore.dev/api/v1/timestamp", "majorApiVersion": 1, "validFor": { "start": "2025-07-04T00:00:00Z" }, "operator": "sigstore.dev" } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } ================================================ FILE: sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json ================================================ { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "https://rekor.sigstore.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27Z" } }, "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" } }, { "baseUrl": "https://log2025-1.rekor.sigstore.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=", "keyDetails": "PKIX_ED25519", "validFor": { "start": "2025-09-23T00:00:00Z" } }, "logId": { "keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" } ] }, "validFor": { "start": "2021-03-07T03:20:29Z", "end": "2022-12-31T23:59:59.999Z" } }, { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" } ] }, "validFor": { "start": "2022-04-13T20:06:15Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstore.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-03-14T00:00:00Z", "end": "2022-10-31T23:59:59.999Z" } }, "logId": { "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" } }, { "baseUrl": "https://ctfe.sigstore.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-10-20T00:00:00Z" } }, "logId": { "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" } } ], "timestampAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore-tsa-selfsigned" }, "uri": "https://timestamp.sigstore.dev/api/v1/timestamp", "certChain": { "certificates": [ { "rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw==" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E" } ] }, "validFor": { "start": "2025-07-04T00:00:00Z" } } ] } ================================================ FILE: sigstore/_utils.py ================================================ # 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. """ Shared utilities. """ from __future__ import annotations import base64 import hashlib import sys from datetime import datetime, timezone from typing import IO, NewType, Union from urllib import parse from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa from cryptography.x509 import ( Certificate, ExtensionNotFound, Version, load_der_x509_certificate, ) from cryptography.x509.oid import ExtendedKeyUsageOID, ExtensionOID from sigstore_models.common.v1 import HashAlgorithm, TimeRange from sigstore import hashes as sigstore_hashes from sigstore.errors import VerificationError if sys.version_info < (3, 11): import importlib_resources as resources else: from importlib import resources PublicKey = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey] PublicKeyTypes = Union[ type[rsa.RSAPublicKey], type[ec.EllipticCurvePublicKey], type[ed25519.Ed25519PublicKey], ] HexStr = NewType("HexStr", str) """ A newtype for `str` objects that contain hexadecimal strings (e.g. `ffabcd00ff`). """ B64Str = NewType("B64Str", str) """ A newtype for `str` objects that contain base64 encoded strings. """ KeyID = NewType("KeyID", bytes) """ A newtype for `bytes` objects that contain a key id. """ def load_pem_public_key( key_pem: bytes, *, types: tuple[PublicKeyTypes, ...] = ( rsa.RSAPublicKey, ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey, ), ) -> PublicKey: """ A specialization of `cryptography`'s `serialization.load_pem_public_key` with a uniform exception type (`VerificationError`) and filtering on valid key types for Sigstore purposes. """ try: key = serialization.load_pem_public_key(key_pem) except Exception as exc: raise VerificationError("could not load PEM-formatted public key") from exc if not isinstance(key, types): raise VerificationError(f"invalid key format: not one of {types}") return key def load_der_public_key( key_der: bytes, *, types: tuple[PublicKeyTypes, ...] = ( rsa.RSAPublicKey, ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey, ), ) -> PublicKey: """ The `load_pem_public_key` specialization, but DER. """ try: key = serialization.load_der_public_key(key_der) except Exception as exc: raise VerificationError("could not load DER-formatted public key") from exc if not isinstance(key, types): raise VerificationError(f"invalid key format: not one of {types}") return key def base64_encode_pem_cert(cert: Certificate) -> B64Str: """ Returns a string containing a base64-encoded PEM-encoded X.509 certificate. """ return B64Str( base64.b64encode(cert.public_bytes(serialization.Encoding.PEM)).decode() ) def cert_der_to_pem(der: bytes) -> str: """ Converts a DER-encoded X.509 certificate into its PEM encoding. Returns a string containing a PEM-encoded X.509 certificate. """ # NOTE: Technically we don't have to round-trip like this, since # the DER-to-PEM transformation is entirely mechanical. cert = load_der_x509_certificate(der) return cert.public_bytes(serialization.Encoding.PEM).decode() def key_id(key: PublicKey) -> KeyID: """ Returns an RFC 6962-style "key ID" for the given public key. See: """ public_bytes = key.public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) return KeyID(hashlib.sha256(public_bytes).digest()) def sha256_digest( input_: bytes | IO[bytes] | sigstore_hashes.Hashed, ) -> sigstore_hashes.Hashed: """ Compute the SHA256 digest of an input stream or buffer or, if given a `Hashed`, return it directly. """ if isinstance(input_, sigstore_hashes.Hashed): return input_ # If the input is already buffered into memory, there's no point in # going back through an I/O abstraction. if isinstance(input_, bytes): return sigstore_hashes.Hashed( digest=hashlib.sha256(input_).digest(), algorithm=HashAlgorithm.SHA2_256 ) return sigstore_hashes.Hashed( digest=_sha256_streaming(input_), algorithm=HashAlgorithm.SHA2_256 ) def _sha256_streaming(io: IO[bytes]) -> bytes: """ Compute the SHA256 of a stream. This function does its own internal buffering, so an unbuffered stream should be supplied for optimal performance. """ # NOTE: This function performs a SHA256 digest over a stream. # The stream's size is not checked, meaning that the stream's source # is implicitly trusted: if an attacker is able to truncate the stream's # source prematurely, then they could conceivably produce a digest # for a partial stream. This in turn could conceivably result # in a valid signature for an unintended (truncated) input. # # This is currently outside of sigstore-python's threat model: we # assume that the stream is trusted. # # See: https://github.com/sigstore/sigstore-python/pull/329#discussion_r1041215972 sha256 = hashlib.sha256() # Per coreutils' ioblksize.h: 128KB performs optimally across a range # of systems in terms of minimizing syscall overhead. view = memoryview(bytearray(128 * 1024)) nbytes = io.readinto(view) # type: ignore[attr-defined] while nbytes: sha256.update(view[:nbytes]) nbytes = io.readinto(view) # type: ignore[attr-defined] return sha256.digest() def read_embedded(name: str, url: str) -> bytes: """ Read a resource for a given TUF repository embedded in this distribution of sigstore-python, returning its contents as bytes. """ embed_dir = parse.quote(url, safe="") b: bytes = resources.files("sigstore._store").joinpath(embed_dir, name).read_bytes() return b def cert_is_ca(cert: Certificate) -> bool: """ Returns `True` if and only if the given `Certificate` is a CA certificate. This function doesn't indicate the trustworthiness of the given `Certificate`, only whether it has the appropriate interior state. This function is **not** naively invertible: users **must** use the dedicated `cert_is_leaf` utility function to determine whether a particular leaf upholds Sigstore's invariants. """ # Only v3 certificates should appear in the context of Sigstore; # earlier versions of X.509 lack extensions and have ambiguous CA # behavior. if cert.version != Version.v3: raise VerificationError(f"invalid X.509 version: {cert.version}") # Valid CA certificates must have the following set: # # * `BasicKeyUsage.keyCertSign` # * `BasicConstraints.ca` # # Any other combination of states is inconsistent and invalid, meaning # that we won't consider the certificate a valid non-CA leaf. try: basic_constraints = cert.extensions.get_extension_for_oid( ExtensionOID.BASIC_CONSTRAINTS ) # BasicConstraints must be marked as critical, per RFC 5280 4.2.1.9. if not basic_constraints.critical: raise VerificationError( "invalid X.509 certificate: non-critical BasicConstraints in CA" ) ca = basic_constraints.value.ca # type: ignore[attr-defined] except ExtensionNotFound: # No BasicConstrains means that this can't possibly be a CA. return False key_cert_sign = False try: key_usage = cert.extensions.get_extension_for_oid(ExtensionOID.KEY_USAGE) key_cert_sign = key_usage.value.key_cert_sign # type: ignore[attr-defined] except ExtensionNotFound: raise VerificationError("invalid X.509 certificate: missing KeyUsage") # If both states are set, this is a CA. if ca and key_cert_sign: return True if not (ca or key_cert_sign): return False # Anything else is an invalid state that should never occur. raise VerificationError( f"invalid X.509 certificate states: KeyUsage.keyCertSign={key_cert_sign}" f", BasicConstraints.ca={ca}" ) def cert_is_root_ca(cert: Certificate) -> bool: """ Returns `True` if and only if the given `Certificate` indicates that it's a root CA. This is **not** a verification function, and it does not establish the trustworthiness of the given certificate. """ # NOTE(ww): This function is obnoxiously long to make the different # states explicit. # Only v3 certificates should appear in the context of Sigstore; # earlier versions of X.509 lack extensions and have ambiguous CA # behavior. if cert.version != Version.v3: raise VerificationError(f"invalid X.509 version: {cert.version}") # Non-CAs can't possibly be root CAs. if not cert_is_ca(cert): return False # A certificate that is its own issuer and signer is considered a root CA. try: cert.verify_directly_issued_by(cert) return True except Exception: return False def cert_is_leaf(cert: Certificate) -> bool: """ Returns `True` if and only if the given `Certificate` is a valid leaf certificate for Sigstore purposes. This means that: * It is not a root or intermediate CA; * It has `KeyUsage.digitalSignature`; * It has `CODE_SIGNING` as an `ExtendedKeyUsage`. This is **not** a verification function, and it does not establish the trustworthiness of the given certificate. """ # Only v3 certificates should appear in the context of Sigstore; # earlier versions of X.509 lack extensions and have ambiguous CA # behavior. if cert.version != Version.v3: raise VerificationError(f"invalid X.509 version: {cert.version}") # CAs are not leaves. if cert_is_ca(cert): return False key_usage = cert.extensions.get_extension_for_oid(ExtensionOID.KEY_USAGE) digital_signature = key_usage.value.digital_signature # type: ignore[attr-defined] if not digital_signature: raise VerificationError( "invalid certificate for Sigstore purposes: missing digital signature usage" ) # Finally, we check to make sure the leaf has an `ExtendedKeyUsages` # extension that includes a codesigning entitlement. Sigstore should # never issue a leaf that doesn't have this extended usage. try: extended_key_usage = cert.extensions.get_extension_for_oid( ExtensionOID.EXTENDED_KEY_USAGE ) return ExtendedKeyUsageOID.CODE_SIGNING in extended_key_usage.value # type: ignore[operator] except ExtensionNotFound: raise VerificationError("invalid X.509 certificate: missing ExtendedKeyUsage") def is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool: """ Given a `period`, checks that the the current time is not before `start`. If `allow_expired` is `False`, also checks that the current time is not after `end`. """ now = datetime.now(timezone.utc) # If there was no validity period specified, the key is always valid. if not period: return True # Active: if the current time is before the starting period, we are not yet # valid. if now < period.start: return False # If we want Expired keys, the key is valid at this point. Otherwise, check # that we are within range. return allow_expired or (period.end is None or now <= period.end) ================================================ FILE: sigstore/dsse/__init__.py ================================================ # 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. """ Functionality for building and manipulating in-toto Statements and DSSE envelopes. """ from __future__ import annotations import base64 import logging from typing import Any, Literal, Optional from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError from sigstore_models.common.v1 import HashAlgorithm from sigstore_models.intoto import Envelope as _Envelope from sigstore_models.intoto import Signature as _Signature from sigstore.errors import Error, VerificationError from sigstore.hashes import Hashed _logger = logging.getLogger(__name__) Digest = Literal["sha256", "sha384", "sha512", "sha3_256", "sha3_384", "sha3_512"] """ NOTE: in-toto's DigestSet contains all kinds of hash algorithms that we intentionally do not support. This model is limited to common members of the SHA-2 and SHA-3 family that are at least as strong as SHA-256. See: """ DigestSet = RootModel[dict[Digest, str]] """ An internal validation model for in-toto subject digest sets. """ class Subject(BaseModel): """ A single in-toto statement subject. """ name: Optional[StrictStr] # noqa: UP045 digest: DigestSet = Field(...) class _Statement(BaseModel): """ An internal validation model for in-toto statements. """ model_config = ConfigDict(populate_by_name=True) type_: Literal["https://in-toto.io/Statement/v1"] = Field(..., alias="_type") subjects: list[Subject] = Field(..., min_length=1, alias="subject") predicate_type: StrictStr = Field(..., alias="predicateType") predicate: Optional[dict[str, Any]] = Field(None, alias="predicate") # noqa: UP045 class Statement: """ Represents an in-toto statement. This type deals with opaque bytes to ensure that the encoding does not change, but Statements are internally checked for conformance against the JSON object layout defined in the in-toto attestation spec. See: """ def __init__(self, contents: bytes | _Statement) -> None: """ Construct a new Statement. This takes an opaque `bytes` containing the statement; use `StatementBuilder` to manually construct an in-toto statement from constituent pieces. """ if isinstance(contents, bytes): self._contents = contents try: self._inner = _Statement.model_validate_json(contents) except ValidationError: raise Error("malformed in-toto statement") else: self._contents = contents.model_dump_json(by_alias=True).encode() self._inner = contents def _matches_digest(self, digest: Hashed) -> bool: """ Returns a boolean indicating whether this in-toto Statement contains a subject matching the given digest. The subject's name is **not** checked. No digests other than SHA256 are currently supported. """ if digest.algorithm != HashAlgorithm.SHA2_256: raise VerificationError(f"unexpected digest algorithm: {digest.algorithm}") for sub in self._inner.subjects: sub_digest = sub.digest.root.get("sha256") if sub_digest is None: continue if sub_digest == digest.digest.hex(): return True return False def _pae(self) -> bytes: """ Construct the PAE encoding for this statement. """ return _pae(Envelope._TYPE, self._contents) class StatementBuilder: """ A builder-style API for constructing in-toto Statements. """ def __init__( self, subjects: list[Subject] | None = None, predicate_type: str | None = None, predicate: dict[str, Any] | None = None, ): """ Create a new `StatementBuilder`. """ self._subjects = subjects or [] self._predicate_type = predicate_type self._predicate = predicate def subjects(self, subjects: list[Subject]) -> StatementBuilder: """ Configure the subjects for this builder. """ self._subjects = subjects return self def predicate_type(self, predicate_type: str) -> StatementBuilder: """ Configure the predicate type for this builder. """ self._predicate_type = predicate_type return self def predicate(self, predicate: dict[str, Any]) -> StatementBuilder: """ Configure the predicate for this builder. """ self._predicate = predicate return self def build(self) -> Statement: """ Build a `Statement` from the builder's state. """ try: stmt = _Statement( type_="https://in-toto.io/Statement/v1", subjects=self._subjects, predicate_type=self._predicate_type, predicate=self._predicate, ) except ValidationError as e: raise Error(f"invalid statement: {e}") return Statement(stmt) class InvalidEnvelope(Error): """ Raised when the associated `Envelope` is invalid in some way. """ class Envelope: """ Represents a DSSE envelope. This class cannot be constructed directly; you must use `sign` or `from_json`. See: """ _TYPE = "application/vnd.in-toto+json" def __init__(self, inner: _Envelope) -> None: """ @private """ self._inner = inner self._verify() def _verify(self) -> None: """ Verify and load the Envelope. """ if len(self._inner.signatures) != 1: raise InvalidEnvelope("envelope must contain exactly one signature") if not self._inner.signatures[0].sig: raise InvalidEnvelope("envelope signature must be non-empty") self._signature_bytes = self._inner.signatures[0].sig @classmethod def _from_json(cls, contents: bytes | str) -> Envelope: """Return a DSSE envelope from the given JSON representation.""" inner = _Envelope.from_json(contents) return cls(inner) def to_json(self) -> str: """ Return a JSON string with this DSSE envelope's contents. """ return self._inner.to_json() def __eq__(self, other: object) -> bool: """Equality for DSSE envelopes.""" if not isinstance(other, Envelope): return NotImplemented return self._inner == other._inner @property def signature(self) -> bytes: """Return the decoded bytes of the Envelope signature.""" return self._signature_bytes def _pae(type_: str, body: bytes) -> bytes: """ Compute the PAE encoding for the given `type_` and `body`. """ # See: # https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md pae = f"DSSEv1 {len(type_)} {type_} ".encode() pae += b" ".join([str(len(body)).encode(), body]) return pae def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope: """ Sign for the given in-toto `Statement`, and encapsulate the resulting signature in a DSSE `Envelope`. """ pae = stmt._pae() _logger.debug(f"DSSE PAE: {pae!r}") signature = key.sign(pae, ec.ECDSA(hashes.SHA256())) return Envelope( _Envelope( payload=base64.b64encode(stmt._contents), payload_type=Envelope._TYPE, signatures=[_Signature(sig=base64.b64encode(signature))], ) ) def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes: """ Verify the given in-toto `Envelope`, returning the verified inner payload. This function does **not** check the envelope's payload type. The caller is responsible for performing this check. """ pae = _pae(evp._inner.payload_type, evp._inner.payload) nsigs = len(evp._inner.signatures) if nsigs != 1: raise VerificationError(f"DSSE: exactly 1 signature allowed, got {nsigs}") signature = evp._inner.signatures[0].sig try: key.verify(signature, pae, ec.ECDSA(hashes.SHA256())) except InvalidSignature: raise VerificationError("DSSE: invalid signature") return evp._inner.payload ================================================ FILE: sigstore/dsse/_predicate.py ================================================ # Copyright 2024 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. """ Models for the predicates used in in-toto statements """ import enum from typing import Any, Literal, Union from pydantic import ( BaseModel, ConfigDict, RootModel, StrictBytes, StrictStr, model_validator, ) from pydantic.alias_generators import to_camel from typing_extensions import Self from sigstore.dsse import Digest class PredicateType(str, enum.Enum): """ Currently supported predicate types """ SLSA_v0_2 = "https://slsa.dev/provenance/v0.2" SLSA_v1_0 = "https://slsa.dev/provenance/v1" # Common models SourceDigest = Literal["sha1", "gitCommit"] DigestSetSource = RootModel[dict[Union[Digest, SourceDigest], str]] """ Same as `dsse.DigestSet` but with `sha1` added. Since this model is not used to verify hashes, but to parse predicates that might contain hashes, we include this weak hash algorithm. This is because provenance providers like GitHub use SHA1 in their predicates to refer to git commit hashes. """ class Predicate(BaseModel): """ Base model for in-toto predicates """ pass class _SLSAConfigBase(BaseModel): """ Base class used to configure the models """ model_config = ConfigDict(alias_generator=to_camel, extra="forbid") # Models for SLSA Provenance v0.2 class BuilderV0_1(_SLSAConfigBase): """ The Builder object used by SLSAPredicateV0_2 """ id: StrictStr class ConfigSource(_SLSAConfigBase): """ The ConfigSource object used by Invocation in v0.2 """ uri: StrictStr | None = None digest: DigestSetSource | None = None entry_point: StrictStr | None = None class Invocation(_SLSAConfigBase): """ The Invocation object used by SLSAPredicateV0_2 """ config_source: ConfigSource | None = None parameters: dict[str, Any] | None = None environment: dict[str, Any] | None = None class Completeness(_SLSAConfigBase): """ The Completeness object used by Metadata in v0.2 """ parameters: bool | None = None environment: bool | None = None materials: bool | None = None class Material(_SLSAConfigBase): """ The Material object used by Metadata in v0.2 """ uri: StrictStr | None = None digest: DigestSetSource | None = None class Metadata(_SLSAConfigBase): """ The Metadata object used by SLSAPredicateV0_2 """ build_invocation_id: StrictStr | None = None build_started_on: StrictStr | None = None build_finished_on: StrictStr | None = None completeness: Completeness | None = None reproducible: bool | None = None class SLSAPredicateV0_2(Predicate, _SLSAConfigBase): """ Represents the predicate object corresponding to the type "https://slsa.dev/provenance/v0.2" """ builder: BuilderV0_1 build_type: StrictStr invocation: Invocation | None = None metadata: Metadata | None = None build_config: dict[str, Any] | None = None materials: list[Material] | None = None # Models for SLSA Provenance v1.0 class ResourceDescriptor(_SLSAConfigBase): """ The ResourceDescriptor object defined defined by the in-toto attestations spec """ name: StrictStr | None = None uri: StrictStr | None = None digest: DigestSetSource | None = None content: StrictBytes | None = None download_location: StrictStr | None = None media_type: StrictStr | None = None annotations: dict[StrictStr, Any] | None = None @model_validator(mode="after") def check_required_fields(self: Self) -> Self: """ While all fields are optional, at least one of the fields `uri`, `digest` or `content` must be present """ if not self.uri and not self.digest and not self.content: raise ValueError( "A ResourceDescriptor MUST specify one of uri, digest or content at a minimum" ) return self class BuilderV1_0(_SLSAConfigBase): """ The Builder object used by RunDetails in v1.0 """ id: StrictStr builder_dependencies: list[ResourceDescriptor] | None = None version: dict[StrictStr, StrictStr] | None = None class BuildMetadata(_SLSAConfigBase): """ The BuildMetadata object used by RunDetails """ invocation_id: StrictStr | None = None started_on: StrictStr | None = None finished_on: StrictStr | None = None class RunDetails(_SLSAConfigBase): """ The RunDetails object used by SLSAPredicateV1_0 """ builder: BuilderV1_0 metadata: BuildMetadata | None = None byproducts: list[ResourceDescriptor] | None = None class BuildDefinition(_SLSAConfigBase): """ The BuildDefinition object used by SLSAPredicateV1_0 """ build_type: StrictStr external_parameters: dict[StrictStr, Any] internal_parameters: dict[str, Any] | None = None resolved_dependencies: list[ResourceDescriptor] | None = None class SLSAPredicateV1_0(Predicate, _SLSAConfigBase): """ Represents the predicate object corresponding to the type "https://slsa.dev/provenance/v1" """ build_definition: BuildDefinition run_details: RunDetails ================================================ FILE: sigstore/errors.py ================================================ # Copyright 2023 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. """ Exceptions. """ import sys from collections.abc import Mapping from logging import Logger from typing import Any, NoReturn class Error(Exception): """Base sigstore exception type. Defines helpers for diagnostics.""" def diagnostics(self) -> str: """Returns human-friendly error information.""" return str(self) def log_and_exit(self, logger: Logger, raise_error: bool = False) -> NoReturn: """Prints all relevant error information to stderr and exits.""" remind_verbose = ( "Raising original exception:" if raise_error else "For detailed error information, run sigstore with the `--verbose` flag." ) logger.error(f"{self.diagnostics()}\n{remind_verbose}") if raise_error: # don't want "during handling another exception" self.__suppress_context__ = True raise self sys.exit(1) class NetworkError(Error): """Raised when a connectivity-related issue occurs.""" def diagnostics(self) -> str: """Returns diagnostics for the error.""" cause_ctx = ( f""" Additional context: {self.__cause__} """ if self.__cause__ else "" ) return ( """\ A network issue occurred. Check your internet connection and try again. """ + cause_ctx ) class TUFError(Error): """Raised when a TUF error occurs.""" def __init__(self, message: str): """Constructs a `TUFError`.""" self.message = message from tuf.api import exceptions _details: Mapping[Any, str] = { exceptions.DownloadError: NetworkError().diagnostics() } def diagnostics(self) -> str: """Returns diagnostics specialized to the wrapped TUF error.""" details = TUFError._details.get( type(self.__context__), "Please check any Sigstore instance related arguments and consider " "reporting the issue at .", ) return f"""\ {self.message}. {details} """ class MetadataError(Error): """Raised when TUF metadata does not conform to the expected structure.""" def diagnostics(self) -> str: """Returns diagnostics for the error.""" return f"""{self}.""" class RootError(Error): """Raised when TUF cannot establish its root of trust.""" def diagnostics(self) -> str: """Returns diagnostics for the error.""" return """\ Unable to establish root of trust. This error may occur when the resources embedded in this distribution of sigstore-python are out of date.""" class VerificationError(Error): """ Raised whenever any phase or subcomponent of Sigstore verification fails. """ class CertValidationError(VerificationError): """ Raised when a TSA certificate chain fails to validate during Sigstore verification. This is used by CLI to hint that an incorrect Sigstore instance may have been used """ ================================================ FILE: sigstore/hashes.py ================================================ # Copyright 2023 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. """ Hashing APIs. """ import rekor_types from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from pydantic import BaseModel from sigstore_models.common.v1 import HashAlgorithm from sigstore.errors import Error class Hashed(BaseModel, frozen=True): """ Represents a hashed value. """ algorithm: HashAlgorithm """ The digest algorithm uses to compute the digest. """ digest: bytes """ The digest representing the hash value. """ def _as_hashedrekord_algorithm(self) -> rekor_types.hashedrekord.Algorithm: """ Returns an appropriate `hashedrekord.Algorithm` for this `Hashed`. """ if self.algorithm == HashAlgorithm.SHA2_256: return rekor_types.hashedrekord.Algorithm.SHA256 raise Error(f"unknown hash algorithm: {self.algorithm}") def _as_prehashed(self) -> Prehashed: """ Returns an appropriate Cryptography `Prehashed` for this `Hashed`. """ if self.algorithm == HashAlgorithm.SHA2_256: return Prehashed(hashes.SHA256()) raise Error(f"unknown hash algorithm: {self.algorithm}") def __str__(self) -> str: """ Returns a str representation of this `Hashed`. """ return f"{self.algorithm.value}:{self.digest.hex()}" ================================================ FILE: sigstore/models.py ================================================ # 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. """ Common models shared between signing and verification. """ from __future__ import annotations import base64 import logging from collections import defaultdict from collections.abc import Iterable from enum import Enum from pathlib import Path from textwrap import dedent from typing import Any import rfc8785 from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509 import ( Certificate, load_der_x509_certificate, ) from pydantic import TypeAdapter from rekor_types import Dsse, Hashedrekord, ProposedEntry from rfc3161_client import TimeStampResponse, decode_timestamp_response from sigstore_models.bundle import v1 as bundle_v1 from sigstore_models.bundle.v1 import Bundle as _Bundle from sigstore_models.bundle.v1 import ( TimestampVerificationData as _TimestampVerificationData, ) from sigstore_models.bundle.v1 import VerificationMaterial as _VerificationMaterial from sigstore_models.common import v1 as common_v1 from sigstore_models.common.v1 import MessageSignature, RFC3161SignedTimestamp from sigstore_models.rekor import v1 as rekor_v1 from sigstore_models.rekor.v1 import TransparencyLogEntry as _TransparencyLogEntry from sigstore_models.trustroot import v1 as trustroot_v1 from sigstore import dsse from sigstore._internal.fulcio.client import FulcioClient from sigstore._internal.merkle import verify_merkle_inclusion from sigstore._internal.rekor import RekorLogSubmitter from sigstore._internal.rekor.checkpoint import verify_checkpoint from sigstore._internal.timestamp import TimestampAuthorityClient from sigstore._internal.trust import ( CertificateAuthority, CTKeyring, Keyring, KeyringPurpose, RekorKeyring, ) from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater from sigstore._utils import KeyID, cert_is_leaf, cert_is_root_ca, is_timerange_valid from sigstore.errors import Error, MetadataError, TUFError, VerificationError # Versions supported by this client REKOR_VERSIONS = [1, 2] TSA_VERSIONS = [1] FULCIO_VERSIONS = [1] OIDC_VERSIONS = [1] _logger = logging.getLogger(__name__) class TransparencyLogEntry: """ Represents a transparency log entry. """ def __init__(self, inner: _TransparencyLogEntry) -> None: """ Creates a new `TransparencyLogEntry` from the given inner object. @private """ self._inner = inner self._validate() def _validate(self) -> None: """ Ensure this transparency log entry is well-formed and upholds our client invariants. """ inclusion_proof: rekor_v1.InclusionProof | None = self._inner.inclusion_proof # This check is required by us as the client, not the # protobuf-specs themselves. if not inclusion_proof or not inclusion_proof.checkpoint: raise InvalidBundle("entry must contain inclusion proof, with checkpoint") def __eq__(self, value: object) -> bool: """ Compares this `TransparencyLogEntry` with another object for equality. Two `TransparencyLogEntry` instances are considered equal if their inner contents are equal. """ if not isinstance(value, TransparencyLogEntry): return NotImplemented return self._inner == value._inner @classmethod def _from_v1_response(cls, dict_: dict[str, Any]) -> TransparencyLogEntry: """ Create a new `TransparencyLogEntry` from the given API response. """ # Assumes we only get one entry back entries = list(dict_.items()) if len(entries) != 1: raise ValueError("Received multiple entries in response") _, entry = entries[0] # Fill in the appropriate kind body_entry: ProposedEntry = TypeAdapter(ProposedEntry).validate_json( base64.b64decode(entry["body"]) ) if not isinstance(body_entry, Hashedrekord | Dsse): raise InvalidBundle("log entry is not of expected type") raw_inclusion_proof = entry["verification"]["inclusionProof"] # NOTE: The type ignores below are a consequence of our Pydantic # modeling: mypy and other typecheckers see `ProtoU64` as `int`, # but it gets coerced from a string due to Protobuf's JSON serialization. inner = _TransparencyLogEntry( log_index=str(entry["logIndex"]), # type: ignore[arg-type] log_id=common_v1.LogId( key_id=base64.b64encode(bytes.fromhex(entry["logID"])) ), kind_version=rekor_v1.KindVersion( kind=body_entry.kind, version=body_entry.api_version ), integrated_time=str(entry["integratedTime"]), # type: ignore[arg-type] inclusion_promise=rekor_v1.InclusionPromise( signed_entry_timestamp=entry["verification"]["signedEntryTimestamp"] ), inclusion_proof=rekor_v1.InclusionProof( log_index=str(raw_inclusion_proof["logIndex"]), # type: ignore[arg-type] root_hash=base64.b64encode( bytes.fromhex(raw_inclusion_proof["rootHash"]) ), tree_size=str(raw_inclusion_proof["treeSize"]), # type: ignore[arg-type] hashes=[ base64.b64encode(bytes.fromhex(h)) for h in raw_inclusion_proof["hashes"] ], checkpoint=rekor_v1.Checkpoint( envelope=raw_inclusion_proof["checkpoint"] ), ), canonicalized_body=entry["body"], ) return cls(inner) def _encode_canonical(self) -> bytes: """ Returns a canonicalized JSON (RFC 8785) representation of the transparency log entry. This encoded representation is suitable for verification against the Signed Entry Timestamp. """ # We might not have an integrated time if our log entry is from rekor # v2, i.e. was integrated synchronously instead of via an # inclusion promise. if self._inner.integrated_time is None: raise ValueError( "can't encode canonical form for SET without integrated time" ) payload: dict[str, int | str] = { "body": base64.b64encode(self._inner.canonicalized_body).decode(), "integratedTime": self._inner.integrated_time, "logID": self._inner.log_id.key_id.hex(), "logIndex": self._inner.log_index, } return rfc8785.dumps(payload) def _verify_set(self, keyring: RekorKeyring) -> None: """ Verify the inclusion promise (Signed Entry Timestamp) for a given transparency log `entry` using the given `keyring`. Fails if the given log entry does not contain an inclusion promise. """ if self._inner.inclusion_promise is None: raise VerificationError("SET: invalid inclusion promise: missing") signed_entry_ts = self._inner.inclusion_promise.signed_entry_timestamp try: keyring.verify( key_id=KeyID(self._inner.log_id.key_id), signature=signed_entry_ts, data=self._encode_canonical(), ) except VerificationError as exc: raise VerificationError(f"SET: invalid inclusion promise: {exc}") def _verify(self, keyring: RekorKeyring) -> None: """ Verifies this log entry. This method performs steps (5), (6), and optionally (7) in the top-level verify API: * Verifies the consistency of the entry with the given bundle; * Verifies the Merkle inclusion proof and its signed checkpoint; * Verifies the inclusion promise, if present. """ verify_merkle_inclusion(self) verify_checkpoint(keyring, self) _logger.debug( f"successfully verified inclusion proof: index={self._inner.log_index}" ) if self._inner.inclusion_promise and self._inner.integrated_time: self._verify_set(keyring) _logger.debug( f"successfully verified inclusion promise: index={self._inner.log_index}" ) class TimestampVerificationData: """ Represents a TimestampVerificationData structure. @private """ def __init__(self, inner: _TimestampVerificationData) -> None: """Init method.""" self._inner = inner self._verify() def _verify(self) -> None: """ Verifies the TimestampVerificationData. It verifies that TimeStamp Responses embedded in the bundle are correctly formed. """ if not (timestamps := self._inner.rfc3161_timestamps): timestamps = [] try: self._signed_ts = [ decode_timestamp_response(ts.signed_timestamp) for ts in timestamps ] except ValueError: raise VerificationError("Invalid Timestamp Response") @property def rfc3161_timestamps(self) -> list[TimeStampResponse]: """Returns a list of signed timestamp.""" return self._signed_ts @classmethod def from_json(cls, raw: str | bytes) -> TimestampVerificationData: """ Deserialize the given timestamp verification data. """ inner = _TimestampVerificationData.from_json(raw) return cls(inner) class VerificationMaterial: """ Represents a VerificationMaterial structure. """ def __init__(self, inner: _VerificationMaterial) -> None: """Init method.""" self._inner = inner @property def timestamp_verification_data(self) -> TimestampVerificationData | None: """ Returns the Timestamp Verification Data, if present. """ if ( self._inner.timestamp_verification_data and self._inner.timestamp_verification_data.rfc3161_timestamps ): return TimestampVerificationData(self._inner.timestamp_verification_data) return None class InvalidBundle(Error): """ Raised when the associated `Bundle` is invalid in some way. """ def diagnostics(self) -> str: """Returns diagnostics for the error.""" return dedent( f"""\ An issue occurred while parsing the Sigstore bundle. The provided bundle is malformed and may have been modified maliciously. Additional context: {self} """ ) class IncompatibleEntry(InvalidBundle): """ Raised when the log entry within the `Bundle` has an incompatible KindVersion. """ def diagnostics(self) -> str: """Returns diagnostics for the error.""" return dedent( f"""\ The provided bundle contains a transparency log entry that is incompatible with this version of sigstore-python. Please upgrade your verifying client. Additional context: {self} """ ) class Bundle: """ Represents a Sigstore bundle. """ class BundleType(str, Enum): """ Known Sigstore bundle media types. """ BUNDLE_0_1 = "application/vnd.dev.sigstore.bundle+json;version=0.1" BUNDLE_0_2 = "application/vnd.dev.sigstore.bundle+json;version=0.2" BUNDLE_0_3_ALT = "application/vnd.dev.sigstore.bundle+json;version=0.3" BUNDLE_0_3 = "application/vnd.dev.sigstore.bundle.v0.3+json" def __str__(self) -> str: """Returns the variant's string value.""" return self.value def __init__(self, inner: _Bundle) -> None: """ Creates a new bundle. This is not a public API; use `from_json` instead. @private """ self._inner = inner self._verify() def _verify(self) -> None: """ Performs various feats of heroism to ensure the bundle is well-formed and upholds invariants, including: * The "leaf" (signing) certificate is present; * There is a inclusion proof present, even if the Bundle's version predates a mandatory inclusion proof. """ # The bundle must have a recognized media type. try: media_type = Bundle.BundleType(self._inner.media_type) except ValueError: raise InvalidBundle(f"unsupported bundle format: {self._inner.media_type}") # Extract the signing certificate. if media_type in ( Bundle.BundleType.BUNDLE_0_3, Bundle.BundleType.BUNDLE_0_3_ALT, ): # For "v3" bundles, the signing certificate is the only one present. if not self._inner.verification_material.certificate: raise InvalidBundle("expected certificate in bundle") leaf_cert = load_der_x509_certificate( self._inner.verification_material.certificate.raw_bytes ) else: # In older bundles, there is an entire pool (misleadingly called # a chain) of certificates, the first of which is the signing # certificate. if not self._inner.verification_material.x509_certificate_chain: raise InvalidBundle("expected certificate chain in bundle") chain = self._inner.verification_material.x509_certificate_chain if not chain.certificates: raise InvalidBundle("expected non-empty certificate chain in bundle") # Per client policy in protobuf-specs: the first entry in the chain # MUST be a leaf certificate, and the rest of the chain MUST NOT # include a root CA or any intermediate CAs that appear in an # independent root of trust. # # We expect some old bundles to violate the rules around root # and intermediate CAs, so we issue warnings and not hard errors # in those cases. leaf_cert, *chain_certs = ( load_der_x509_certificate(cert.raw_bytes) for cert in chain.certificates ) if not cert_is_leaf(leaf_cert): raise InvalidBundle( "bundle contains an invalid leaf or non-leaf certificate in the leaf position" ) for chain_cert in chain_certs: # TODO: We should also retrieve the root of trust here and # cross-check against it. if cert_is_root_ca(chain_cert): _logger.warning( "this bundle contains a root CA, making it subject to misuse" ) self._signing_certificate = leaf_cert # Extract the log entry. For the time being, we expect # bundles to only contain a single log entry. tlog_entries = self._inner.verification_material.tlog_entries if len(tlog_entries) != 1: raise InvalidBundle("expected exactly one log entry in bundle") tlog_entry = tlog_entries[0] if tlog_entry.kind_version.version not in ["0.0.1", "0.0.2"]: raise IncompatibleEntry( f"Expected log entry version 0.0.1 - 0.0.2, got {tlog_entry.kind_version.version}" ) # Handling of inclusion promises and proofs varies between bundle # format versions: # # * For 0.1, an inclusion promise is required; the client # MUST verify the inclusion promise. # The inclusion proof is NOT required. If provided, it might NOT # contain a checkpoint; in this case, we ignore it (since it's # useless without one). # # * For 0.2+, an inclusion proof is required; the client MUST # verify the inclusion proof. The inclusion prof MUST contain # a checkpoint. # # The inclusion promise is NOT required if another source of signed # time (such as a signed timestamp) is present. If no other source # of signed time is present, then the inclusion promise MUST be # present. # # Before all of this, we require that the inclusion proof be present # (when constructing the LogEntry). log_entry = TransparencyLogEntry(tlog_entry) if media_type == Bundle.BundleType.BUNDLE_0_1: if not log_entry._inner.inclusion_promise: raise InvalidBundle("bundle must contain an inclusion promise") if not log_entry._inner.inclusion_proof.checkpoint: _logger.debug( "0.1 bundle contains inclusion proof without checkpoint; ignoring" ) else: if not log_entry._inner.inclusion_proof.checkpoint: raise InvalidBundle("expected checkpoint in inclusion proof") if ( not log_entry._inner.inclusion_promise and not self.verification_material.timestamp_verification_data ): raise InvalidBundle( "bundle must contain an inclusion promise or signed timestamp(s)" ) self._log_entry = log_entry @property def signing_certificate(self) -> Certificate: """Returns the bundle's contained signing (i.e. leaf) certificate.""" return self._signing_certificate @property def log_entry(self) -> TransparencyLogEntry: """ Returns the bundle's log entry, containing an inclusion proof (with checkpoint) and an inclusion promise (if the latter is present). """ return self._log_entry @property def _dsse_envelope(self) -> dsse.Envelope | None: """ Returns the DSSE envelope within this Bundle as a `dsse.Envelope`. @private """ if self._inner.dsse_envelope is not None: return dsse.Envelope(self._inner.dsse_envelope) return None @property def signature(self) -> bytes: """ Returns the signature bytes of this bundle. Either from the DSSE Envelope or from the message itself. """ return ( self._dsse_envelope.signature if self._dsse_envelope else self._inner.message_signature.signature # type: ignore[union-attr] ) @property def verification_material(self) -> VerificationMaterial: """ Returns the bundle's verification material. """ return VerificationMaterial(self._inner.verification_material) @classmethod def from_json(cls, raw: bytes | str) -> Bundle: """ Deserialize the given Sigstore bundle. """ try: inner = _Bundle.from_json(raw) except ValueError as exc: raise InvalidBundle(f"failed to load bundle: {exc}") return cls(inner) def to_json(self) -> str: """ Return a JSON encoding of this bundle. """ return self._inner.to_json() def _to_parts( self, ) -> tuple[Certificate, MessageSignature | dsse.Envelope, TransparencyLogEntry]: """ Decompose the `Bundle` into its core constituent parts. @private """ content: MessageSignature | dsse.Envelope if self._dsse_envelope: content = self._dsse_envelope else: content = self._inner.message_signature # type: ignore[assignment] return (self.signing_certificate, content, self.log_entry) @classmethod def from_parts( cls, cert: Certificate, sig: bytes, log_entry: TransparencyLogEntry ) -> Bundle: """ Construct a Sigstore bundle (of `hashedrekord` type) from its constituent parts. """ return cls._from_parts( cert, MessageSignature(signature=base64.b64encode(sig)), log_entry ) @classmethod def _from_parts( cls, cert: Certificate, content: MessageSignature | dsse.Envelope, log_entry: TransparencyLogEntry, signed_timestamp: list[TimeStampResponse] | None = None, ) -> Bundle: """ @private """ timestamp_verifcation_data = bundle_v1.TimestampVerificationData( rfc3161_timestamps=[] ) if signed_timestamp is not None: timestamp_verifcation_data.rfc3161_timestamps.extend( [ RFC3161SignedTimestamp( signed_timestamp=base64.b64encode(response.as_bytes()) ) for response in signed_timestamp ] ) # Fill in the appropriate variant. message_signature = None dsse_envelope = None if isinstance(content, MessageSignature): message_signature = content else: dsse_envelope = content._inner inner = _Bundle( media_type=Bundle.BundleType.BUNDLE_0_3.value, verification_material=bundle_v1.VerificationMaterial( certificate=common_v1.X509Certificate( raw_bytes=base64.b64encode(cert.public_bytes(Encoding.DER)) ), tlog_entries=[log_entry._inner], timestamp_verification_data=timestamp_verifcation_data, ), message_signature=message_signature, dsse_envelope=dsse_envelope, ) return cls(inner) class SigningConfig: """ Signing configuration for a Sigstore instance. """ class SigningConfigType(str, Enum): """ Known Sigstore signing config media types. """ SIGNING_CONFIG_0_2 = "application/vnd.dev.sigstore.signingconfig.v0.2+json" def __str__(self) -> str: """Returns the variant's string value.""" return self.value def __init__( self, inner: trustroot_v1.SigningConfig, tlog_version: int | None = None ): """ Construct a new `SigningConfig`. tlog_version is an optional argument that enforces that only specified versions of rekor are included in the transparency logs. @api private """ self._inner = inner # must have a recognized media type. try: SigningConfig.SigningConfigType(self._inner.media_type) except ValueError: raise Error(f"unsupported signing config format: {self._inner.media_type}") # Create lists of service protos that are valid, selected by the service # configuration & supported by this client if tlog_version is None: tlog_versions = REKOR_VERSIONS else: tlog_versions = [tlog_version] self._tlogs = self._get_valid_services( self._inner.rekor_tlog_urls, tlog_versions, self._inner.rekor_tlog_config ) if not self._tlogs: raise Error("No valid Rekor transparency log found in signing config") self._tsas = self._get_valid_services( self._inner.tsa_urls, TSA_VERSIONS, self._inner.tsa_config ) self._fulcios = self._get_valid_services( self._inner.ca_urls, FULCIO_VERSIONS, None ) if not self._fulcios: raise Error("No valid Fulcio CA found in signing config") self._oidcs = self._get_valid_services( self._inner.oidc_urls, OIDC_VERSIONS, None ) @classmethod def from_file( cls, path: str, ) -> SigningConfig: """Create a new signing config from file""" inner = trustroot_v1.SigningConfig.from_json(Path(path).read_bytes()) return cls(inner) @staticmethod def _get_valid_services( services: list[trustroot_v1.Service], supported_versions: list[int], config: trustroot_v1.ServiceConfiguration | None, ) -> list[trustroot_v1.Service]: """Return supported services, taking SigningConfig restrictions into account""" # split services by operator, only include valid services services_by_operator: dict[str, list[trustroot_v1.Service]] = defaultdict(list) for service in services: if service.major_api_version not in supported_versions: continue if not is_timerange_valid(service.valid_for, allow_expired=False): continue services_by_operator[service.operator].append(service) # build a list of services but make sure we only include one service per operator # and use the highest version available for that operator result: list[trustroot_v1.Service] = [] for op_services in services_by_operator.values(): op_services.sort(key=lambda s: s.major_api_version) result.append(op_services[-1]) # Depending on ServiceSelector, prune the result list if not config or config.selector == trustroot_v1.ServiceSelector.ALL: return result # handle EXACT and ANY selectors count = ( config.count if config.selector == trustroot_v1.ServiceSelector.EXACT and config.count else 1 ) if ( config.selector == trustroot_v1.ServiceSelector.EXACT and len(result) < count ): raise ValueError( f"Expected {count} services in signing config, found {len(result)}" ) return result[:count] def get_tlogs(self) -> list[RekorLogSubmitter]: """ Returns the rekor transparency log clients to sign with. """ result: list[RekorLogSubmitter] = [] for tlog in self._tlogs: if tlog.major_api_version == 1: from sigstore._internal.rekor.client import RekorClient result.append(RekorClient(tlog.url)) elif tlog.major_api_version == 2: from sigstore._internal.rekor.client_v2 import RekorV2Client result.append(RekorV2Client(tlog.url)) else: raise AssertionError(f"Unexpected Rekor v{tlog.major_api_version}") return result def get_fulcio(self) -> FulcioClient: """ Returns a Fulcio client to get a signing certificate from """ return FulcioClient(self._fulcios[0].url) def get_oidc_url(self) -> str: """ Returns url for the OIDC provider that client should use to interactively authenticate. """ if not self._oidcs: raise Error("No valid OIDC provider found in signing config") return self._oidcs[0].url def get_tsas(self) -> list[TimestampAuthorityClient]: """ Returns timestamp authority clients for urls configured in signing config. """ return [TimestampAuthorityClient(s.url) for s in self._tsas] class TrustedRoot: """ The cryptographic root(s) of trust for a Sigstore instance. """ class TrustedRootType(str, Enum): """ Known Sigstore trusted root media types. """ TRUSTED_ROOT_0_1 = "application/vnd.dev.sigstore.trustedroot+json;version=0.1" def __str__(self) -> str: """Returns the variant's string value.""" return self.value def __init__(self, inner: trustroot_v1.TrustedRoot): """ Construct a new `TrustedRoot`. @api private """ self._inner = inner self._verify() def _verify(self) -> None: """ Performs various feats of heroism to ensure that the trusted root is well-formed. """ # The trusted root must have a recognized media type. try: TrustedRoot.TrustedRootType(self._inner.media_type) except ValueError: raise Error(f"unsupported trusted root format: {self._inner.media_type}") @classmethod def from_file( cls, path: str, ) -> TrustedRoot: """Create a new trust root from file""" inner = trustroot_v1.TrustedRoot.from_json(Path(path).read_bytes()) return cls(inner) def _get_tlog_keys( self, tlogs: list[trustroot_v1.TransparencyLogInstance], purpose: KeyringPurpose ) -> Iterable[common_v1.PublicKey]: """ Yields an iterator of public keys for transparency log instances that are suitable for `purpose`. """ allow_expired = purpose is KeyringPurpose.VERIFY for tlog in tlogs: if not is_timerange_valid( tlog.public_key.valid_for, allow_expired=allow_expired ): continue yield tlog.public_key def rekor_keyring(self, purpose: KeyringPurpose) -> RekorKeyring: """Return keyring with keys for Rekor.""" keys: list[common_v1.PublicKey] = list( self._get_tlog_keys(self._inner.tlogs, purpose) ) if len(keys) == 0: raise MetadataError("Did not find any Rekor keys in trusted root") return RekorKeyring(Keyring(keys)) def ct_keyring(self, purpose: KeyringPurpose) -> CTKeyring: """Return keyring with key for CTFE.""" ctfes: list[common_v1.PublicKey] = list( self._get_tlog_keys(self._inner.ctlogs, purpose) ) if not ctfes: raise MetadataError("CTFE keys not found in trusted root") return CTKeyring(Keyring(ctfes)) def get_fulcio_certs(self) -> list[Certificate]: """Return the Fulcio certificates.""" certs: list[Certificate] = [] # Return expired certificates too: they are expired now but may have # been active when the certificate was used to sign. for authority in self._inner.certificate_authorities: certificate_authority = CertificateAuthority(authority) certs.extend(certificate_authority.certificates(allow_expired=True)) if not certs: raise MetadataError("Fulcio certificates not found in trusted root") return certs def get_timestamp_authorities(self) -> list[CertificateAuthority]: """ Return the TSA present in the trusted root. This list may be empty and in this case, no timestamp verification can be performed. """ certificate_authorities: list[CertificateAuthority] = [ CertificateAuthority(cert_chain) for cert_chain in self._inner.timestamp_authorities ] return certificate_authorities class ClientTrustConfig: """ Represents a Sigstore client's trust configuration, including a root of trust. """ class ClientTrustConfigType(str, Enum): """ Known Sigstore client trust config media types. """ CONFIG_0_1 = "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json" def __str__(self) -> str: """Returns the variant's string value.""" return self.value @classmethod def from_json(cls, raw: str) -> ClientTrustConfig: """ Deserialize the given client trust config. """ inner = trustroot_v1.ClientTrustConfig.from_json(raw) return cls(inner) @classmethod def production( cls, offline: bool = False, ) -> ClientTrustConfig: """Create new trust config from Sigstore production TUF repository. If `offline`, will use data in local TUF cache. Otherwise will update the data from remote TUF repository. """ return cls.from_tuf(DEFAULT_TUF_URL, offline) @classmethod def staging( cls, offline: bool = False, ) -> ClientTrustConfig: """Create new trust config from Sigstore staging TUF repository. If `offline`, will use data in local TUF cache. Otherwise will update the data from remote TUF repository. """ return cls.from_tuf(STAGING_TUF_URL, offline) @classmethod def from_tuf( cls, url: str, offline: bool = False, bootstrap_root: Path | None = None, ) -> ClientTrustConfig: """Create a new trust config from a TUF repository. If `offline`, will use data in local TUF cache. Otherwise will update the trust config from remote TUF repository. """ updater = TrustUpdater(url, offline, bootstrap_root) tr_path = updater.get_trusted_root_path() inner_tr = trustroot_v1.TrustedRoot.from_json(Path(tr_path).read_bytes()) try: sc_path = updater.get_signing_config_path() inner_sc = trustroot_v1.SigningConfig.from_json(Path(sc_path).read_bytes()) except TUFError as e: raise e return cls( trustroot_v1.ClientTrustConfig( media_type=ClientTrustConfig.ClientTrustConfigType.CONFIG_0_1.value, trusted_root=inner_tr, signing_config=inner_sc, ) ) def __init__(self, inner: trustroot_v1.ClientTrustConfig) -> None: """ @api private """ self._inner = inner # This can be used to enforce a specific rekor major version in signingconfig self.force_tlog_version: int | None = None @property def trusted_root(self) -> TrustedRoot: """ Return the interior root of trust, as a `TrustedRoot`. """ return TrustedRoot(self._inner.trusted_root) @property def signing_config(self) -> SigningConfig: """ Return the interior root of trust, as a `SigningConfig`. """ return SigningConfig( self._inner.signing_config, tlog_version=self.force_tlog_version ) ================================================ FILE: sigstore/oidc.py ================================================ # 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. """ API for retrieving OIDC tokens. """ from __future__ import annotations import logging import sys import time import urllib.parse import webbrowser from datetime import datetime, timezone from typing import NoReturn import id import jwt import requests from pydantic import BaseModel, StrictStr from sigstore._internal import USER_AGENT from sigstore.errors import Error, NetworkError # See: https://github.com/sigstore/fulcio/blob/b2186c0/pkg/config/config.go#L182-L201 _KNOWN_OIDC_ISSUERS = { "https://accounts.google.com": "email", "https://oauth2.sigstore.dev/auth": "email", "https://oauth2.sigstage.dev/auth": "email", "https://token.actions.githubusercontent.com": "sub", } _DEFAULT_CLIENT_ID = "sigstore" class _OpenIDConfiguration(BaseModel): """ Represents a (subset) of the fields provided by an OpenID Connect provider's `.well-known/openid-configuration` response, as defined by OpenID Connect Discovery. See: """ authorization_endpoint: StrictStr token_endpoint: StrictStr class ExpiredIdentity(Exception): """An error raised when an identity token is expired.""" class IdentityToken: """ An OIDC "identity", corresponding to an underlying OIDC token with a sensible subject, issuer, and audience for Sigstore purposes. """ def __init__(self, raw_token: str, client_id: str = _DEFAULT_CLIENT_ID) -> None: """ Create a new `IdentityToken` from the given OIDC token. """ self._raw_token = raw_token # NOTE: The lack of verification here is intentional, and is part of # Sigstore's verification model: clients like sigstore-python are # responsible only for forwarding the OIDC identity to Fulcio for # certificate binding and issuance. try: self._unverified_claims = jwt.decode( raw_token, options={ "verify_signature": False, "verify_aud": True, "verify_iat": True, "verify_exp": True, # These claims are required by OpenID Connect, so # we can strongly enforce their presence. # See: https://openid.net/specs/openid-connect-basic-1_0.html#IDToken "require": ["aud", "sub", "iat", "exp", "iss"], }, audience=client_id, # NOTE: This leeway shouldn't be strictly necessary, but is # included to preempt any (small) skew between the host # and the originating IdP. leeway=5, ) except Exception as exc: raise IdentityError( "Identity token is malformed or missing claims" ) from exc self._iss: str = self._unverified_claims["iss"] self._nbf: int | None = self._unverified_claims.get("nbf") self._exp: int = self._unverified_claims["exp"] # Fail early if this token isn't within its validity period. if not self.in_validity_period(): raise IdentityError("Identity token is not within its validity period") # When verifying the private key possession proof, Fulcio uses # different claims depending on the token's issuer. # We currently special-case a handful of these, and fall back # on signing the "sub" claim otherwise. identity_claim = _KNOWN_OIDC_ISSUERS.get(self.issuer) if identity_claim is not None: if identity_claim not in self._unverified_claims: raise IdentityError( f"Identity token is missing the required {identity_claim!r} claim" ) self._identity = str(self._unverified_claims.get(identity_claim)) else: try: self._identity = str(self._unverified_claims["sub"]) except KeyError: raise IdentityError( "Identity token is missing the required 'sub' claim" ) # This identity token might have been retrieved directly from # an identity provider, or it might be a "federated" identity token # retrieved from a federated IdP (e.g., Sigstore's own Dex instance). # In the latter case, the claims will also include a `federated_claims` # set, which in turn should include a `connector_id` that reflects # the "real" token issuer. We retrieve this, despite technically # being an implementation detail, because it has value to client # users: a client might want to make sure that its user is identifying # with a *particular* IdP, which means that they need to pierce the # federation layer to check which IdP is actually being used. self._federated_issuer: str | None = None federated_claims = self._unverified_claims.get("federated_claims") if federated_claims is not None: if not isinstance(federated_claims, dict): raise IdentityError( "unexpected claim type: federated_claims is not a dict" ) federated_issuer = federated_claims.get("connector_id") if federated_issuer is not None: if not isinstance(federated_issuer, str): raise IdentityError( "unexpected claim type: federated_claims.connector_id is not a string" ) self._federated_issuer = federated_issuer def in_validity_period(self) -> bool: """ Returns whether or not this `Identity` is currently within its self-stated validity period. NOTE: As noted in `Identity.__init__`, this is not a verifying wrapper; the check here only asserts whether the *unverified* identity's claims are within their validity period. """ now = datetime.now(timezone.utc).timestamp() if self._nbf is not None: return self._nbf <= now < self._exp else: return now < self._exp @property def identity(self) -> str: """ Returns this `IdentityToken`'s underlying "subject". Note that this is **not** always the `sub` claim in the corresponding identity token: depending onm the token's issuer, it may be a *different* claim, such as `email`. This corresponds to the Sigstore ecosystem's behavior, e.g. in each issued certificate's SAN. """ return self._identity @property def issuer(self) -> str: """ Returns a URL identifying this `IdentityToken`'s issuer. """ return self._iss @property def federated_issuer(self) -> str: """ Returns a URL identifying the **federated** issuer for any Sigstore certificate issued against this identity token. The behavior of this field is slightly subtle: for non-federated identity providers (like a token issued directly by Google's IdP) it should be exactly equivalent to `IdentityToken.issuer`. For federated issuers (like Sigstore's own federated IdP) it should be equivalent to the underlying federated issuer's URL, which is kept in an implementation-defined claim. This attribute exists so that clients who wish to inspect the expected underlying issuer of their certificates can do so without relying on implementation-specific behavior. """ if self._federated_issuer is not None: return self._federated_issuer return self.issuer def __str__(self) -> str: """ Returns the underlying OIDC token for this identity. That this token is secret in nature and **MUST NOT** be disclosed. """ return self._raw_token class IssuerError(Exception): """ Raised on any communication or format error with an OIDC issuer. """ pass class Issuer: """ Represents an OIDC issuer (IdP). """ def __init__(self, base_url: str) -> None: """ Create a new `Issuer` from the given base URL. This URL is used to locate an OpenID Connect configuration file, which is then used to bootstrap the issuer's state (such as authorization and token endpoints). """ self.session = requests.Session() self.session.headers.update({"User-Agent": USER_AGENT}) oidc_config_url = urllib.parse.urljoin( f"{base_url}/", ".well-known/openid-configuration" ) try: resp: requests.Response = self.session.get(oidc_config_url, timeout=30) except (requests.ConnectionError, requests.Timeout) as exc: raise NetworkError from exc try: resp.raise_for_status() except requests.HTTPError as http_error: raise IssuerError from http_error try: # We don't generally expect this to fail (since the provider should # return a non-success HTTP code which we catch above), but we # check just in case we have a misbehaving OIDC issuer. self.oidc_config = _OpenIDConfiguration.model_validate(resp.json()) except ValueError as exc: raise IssuerError(f"OIDC issuer returned invalid configuration: {exc}") def identity_token( # nosec: B107 self, client_id: str = _DEFAULT_CLIENT_ID, client_secret: str = "", force_oob: bool = False, ) -> IdentityToken: """ Retrieves and returns an `IdentityToken` from the current `Issuer`, via OAuth. This function blocks on user interaction. The `force_oob` flag controls the kind of flow performed. When `False` (the default), this function attempts to open the user's web browser before falling back to an out-of-band flow. When `True`, the out-of-band flow is always used. """ # This function and the components that it relies on are based off of: # https://github.com/psteniusubi/python-sample from sigstore._internal.oidc.oauth import _OAuthFlow code: str with _OAuthFlow(client_id, client_secret, self) as server: # Launch web browser if not force_oob and webbrowser.open(server.base_uri): print("Waiting for browser interaction...", file=sys.stderr) else: server.enable_oob() print( f"Go to the following link in a browser:\n\n\t{server.auth_endpoint}", file=sys.stderr, ) if not server.is_oob(): # Wait until the redirect server populates the response while server.auth_response is None: time.sleep(0.1) auth_error = server.auth_response.get("error") if auth_error is not None: raise IdentityError( f"Error response from auth endpoint: {auth_error[0]}" ) if server.auth_response["state"][0] != server.oauth_session.state: raise IdentityError("OAuth state mismatch") code = server.auth_response["code"][0] else: # In the out-of-band case, we wait until the user provides the code code = input("Enter verification code: ") # Provide code to token endpoint data = { "grant_type": "authorization_code", "redirect_uri": server.redirect_uri, "code": code, "code_verifier": server.oauth_session.code_verifier, } auth = ( client_id, client_secret, ) logging.debug(f"PAYLOAD: data={data}") try: resp = self.session.post( self.oidc_config.token_endpoint, data=data, auth=auth, timeout=30, ) except (requests.ConnectionError, requests.Timeout) as exc: raise NetworkError from exc try: resp.raise_for_status() except requests.HTTPError as http_error: raise IdentityError( f"Token request failed with {resp.status_code}" ) from http_error token_json = resp.json() token_error = token_json.get("error") if token_error is not None: raise IdentityError(f"Error response from token endpoint: {token_error}") return IdentityToken(token_json["access_token"], client_id) class IdentityError(Error): """ Wraps `id`'s IdentityError. """ @classmethod def raise_from_id(cls, exc: id.IdentityError) -> NoReturn: """Raises a wrapped IdentityError from the provided `id.IdentityError`.""" raise cls(str(exc)) from exc def diagnostics(self) -> str: """Returns diagnostics for the error.""" if isinstance(self.__cause__, id.GitHubOidcPermissionCredentialError): return f""" Insufficient permissions for GitHub Actions workflow. The most common reason for this is incorrect configuration of the top-level `permissions` setting of the workflow YAML file. It should be configured like so: permissions: id-token: write Relevant documentation here: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings Another possible reason is that the workflow run has been triggered by a PR from a forked repository. PRs from forked repositories typically cannot be granted write access. Relevant documentation here: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token Additional context: {self.__cause__} """ else: return f""" An issue occurred with ambient credential detection. Additional context: {self} """ def detect_credential(client_id: str = _DEFAULT_CLIENT_ID) -> str | None: """Calls `id.detect_credential`, but wraps exceptions with our own exception type.""" try: return id.detect_credential(client_id) except id.IdentityError as exc: IdentityError.raise_from_id(exc) ================================================ FILE: sigstore/py.typed ================================================ ================================================ FILE: sigstore/sign.py ================================================ # 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. """ API for signing artifacts. Example: ```python from pathlib import Path from sigstore.models import ClientTrustConfig from sigstore.oidc import Issuer from sigstore.sign import SigningContext artifact_path = Path("README.md") # Construct OIDC Issuer and SigningContext for the # Sigstore Public Good instance trust_config = ClientTrustConfig.production() issuer = Issuer(trust_config.signing_config.get_oidc_url()) context = SigningContext.from_trust_config(trust_config) # Get an identity token from OIDC provider using interactive auth token = issuer.identity_token() # Sign artifact with the identity with context.signer(token, cache = True) as signer: bundle = signer.sign_artifact(artifact_path.read_bytes()) with Path("README.md.sigstore.json").open("w") as f: f.write(bundle.to_json()) ``` """ from __future__ import annotations import base64 import logging from collections.abc import Iterator from contextlib import contextmanager from datetime import datetime, timezone import cryptography.x509 as x509 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import NameOID from sigstore_models.common.v1 import HashOutput, MessageSignature from sigstore import dsse from sigstore import hashes as sigstore_hashes from sigstore._internal.fulcio import ( ExpiredCertificate, FulcioClient, ) from sigstore._internal.rekor import EntryRequestBody, RekorLogSubmitter from sigstore._internal.sct import verify_sct from sigstore._internal.timestamp import TimestampAuthorityClient, TimestampError from sigstore._internal.trust import KeyringPurpose from sigstore._utils import sha256_digest from sigstore.models import Bundle, ClientTrustConfig, TrustedRoot from sigstore.oidc import ExpiredIdentity, IdentityToken _logger = logging.getLogger(__name__) class Signer: """ The primary API for signing operations. """ def __init__( self, identity_token: IdentityToken, signing_ctx: SigningContext, cache: bool = True, ) -> None: """ Create a new `Signer`. `identity_token` is the identity token used to request a signing certificate from Fulcio. `signing_ctx` is a `SigningContext` that keeps information about the signing configuration. `cache` determines whether the signing certificate and ephemeral private key should be reused (until the certificate expires) to sign different artifacts. Default is `True`. """ self._identity_token = identity_token self._signing_ctx: SigningContext = signing_ctx self.__cached_private_key: ec.EllipticCurvePrivateKey | None = None self.__cached_signing_certificate: x509.Certificate | None = None if cache: _logger.debug("Generating ephemeral keys...") self.__cached_private_key = ec.generate_private_key(ec.SECP256R1()) _logger.debug("Requesting ephemeral certificate...") self.__cached_signing_certificate = self._signing_cert() @property def _private_key(self) -> ec.EllipticCurvePrivateKey: """Get or generate a signing key.""" if self.__cached_private_key is None: _logger.debug("no cached key; generating ephemeral key") return ec.generate_private_key(ec.SECP256R1()) return self.__cached_private_key def _signing_cert( self, ) -> x509.Certificate: """ Get or request a signing certificate from Fulcio. Internally, this performs a CSR against Fulcio and verifies that the returned certificate is present in Fulcio's CT log. """ # Our CSR cannot possibly succeed if our underlying identity token # is expired. if not self._identity_token.in_validity_period(): raise ExpiredIdentity # If it exists, verify if the current certificate is expired if self.__cached_signing_certificate: not_valid_after = self.__cached_signing_certificate.not_valid_after_utc if datetime.now(timezone.utc) > not_valid_after: raise ExpiredCertificate return self.__cached_signing_certificate else: _logger.debug("Retrieving signed certificate...") # Build an X.509 Certificate Signing Request builder = ( x509.CertificateSigningRequestBuilder() .subject_name( x509.Name( [ x509.NameAttribute( NameOID.EMAIL_ADDRESS, self._identity_token._identity ), ] ) ) .add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, ) ) certificate_request = builder.sign(self._private_key, hashes.SHA256()) certificate_response = self._signing_ctx._fulcio.signing_cert.post( certificate_request, self._identity_token ) verify_sct( certificate_response.cert, certificate_response.chain, self._signing_ctx._trusted_root.ct_keyring(KeyringPurpose.SIGN), ) _logger.debug("Successfully verified SCT...") return certificate_response.cert def _finalize_sign( self, cert: x509.Certificate, content: MessageSignature | dsse.Envelope, proposed_entry: EntryRequestBody, ) -> Bundle: """ Perform the common "finalizing" steps in a Sigstore signing flow. """ # If the user provided TSA urls, timestamps the response signed_timestamp = [] for tsa_client in self._signing_ctx._tsa_clients: try: signed_timestamp.append(tsa_client.request_timestamp(content.signature)) except TimestampError as e: _logger.warning( f"Unable to use {tsa_client.url} to timestamp the bundle. Failed with {e}" ) # Submit the proposed entry to the transparency log entry = self._signing_ctx._rekor.create_entry(proposed_entry) _logger.debug( f"Transparency log entry created with index: {entry._inner.log_index}" ) return Bundle._from_parts(cert, content, entry, signed_timestamp) def sign_dsse( self, input_: dsse.Statement, ) -> Bundle: """ Sign the given in-toto statement as a DSSE envelope, and return a `Bundle` containing the signed result. This API is **only** for in-toto statements; to sign arbitrary artifacts, use `sign_artifact` instead. """ cert = self._signing_cert() # Sign the statement, producing a DSSE envelope content = dsse._sign(self._private_key, input_) # Create the proposed DSSE log entry proposed_entry = self._signing_ctx._rekor._build_dsse_request( envelope=content, certificate=cert ) return self._finalize_sign(cert, content, proposed_entry) def sign_artifact( self, input_: bytes | sigstore_hashes.Hashed, ) -> Bundle: """ Sign an artifact, and return a `Bundle` corresponding to the signed result. The input can be one of two forms: 1. A `bytes` buffer; 2. A `Hashed` object, containing a pre-hashed input (e.g., for inputs that are too large to buffer into memory). Regardless of the input format, the signing operation will produce a `hashedrekord` entry within the bundle. No other entry types are supported by this API. """ cert = self._signing_cert() # Sign artifact hashed_input = sha256_digest(input_) artifact_signature = self._private_key.sign( hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed()) ) content = MessageSignature( message_digest=HashOutput( algorithm=hashed_input.algorithm, digest=base64.b64encode(hashed_input.digest), ), signature=base64.b64encode(artifact_signature), ) # Create the proposed hashedrekord entry proposed_entry = self._signing_ctx._rekor._build_hashed_rekord_request( hashed_input=hashed_input, signature=artifact_signature, certificate=cert ) return self._finalize_sign(cert, content, proposed_entry) class SigningContext: """ Keep a context between signing operations. """ def __init__( self, *, fulcio: FulcioClient, rekor: RekorLogSubmitter, trusted_root: TrustedRoot, tsa_clients: list[TimestampAuthorityClient] | None = None, ): """ Create a new `SigningContext`. `fulcio` is a `FulcioClient` capable of connecting to a Fulcio instance and returning signing certificates. `rekor` is a `RekorClient` capable of connecting to a Rekor instance and creating transparency log entries. """ self._fulcio = fulcio self._rekor = rekor self._trusted_root = trusted_root self._tsa_clients = tsa_clients or [] @classmethod def from_trust_config(cls, trust_config: ClientTrustConfig) -> SigningContext: """ Create a `SigningContext` from the given `ClientTrustConfig`. @api private """ signing_config = trust_config.signing_config return cls( fulcio=signing_config.get_fulcio(), rekor=signing_config.get_tlogs()[0], trusted_root=trust_config.trusted_root, tsa_clients=signing_config.get_tsas(), ) @contextmanager def signer( self, identity_token: IdentityToken, *, cache: bool = True ) -> Iterator[Signer]: """ A context manager for signing operations. `identity_token` is the identity token passed to the `Signer` instance and used to request a signing certificate from Fulcio. `cache` determines whether the signing certificate and ephemeral private key generated by the `Signer` instance should be reused (until the certificate expires) to sign different artifacts. Default is `True`. """ yield Signer(identity_token, self, cache) ================================================ FILE: sigstore/verify/__init__.py ================================================ # 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. """ API for verifying artifact signatures. Example: ```python import base64 from pathlib import Path from sigstore.models import Bundle from sigstore.verify import Verifier from sigstore.verify.policy import Identity # The input to verify input_ = Path("foo.txt").read_bytes() # The bundle to verify with bundle = Bundle.from_json(Path("foo.txt.sigstore.json").read_bytes()) verifier = Verifier.production() result = verifier.verify( input_, bundle, Identity( identity="foo@bar.com", issuer="https://accounts.google.com", ), ) print(result) ``` """ from sigstore.verify import policy, verifier from sigstore.verify.verifier import Verifier __all__ = [ "Verifier", "policy", "verifier", ] ================================================ FILE: sigstore/verify/policy.py ================================================ # 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. """ APIs for describing identity verification "policies", which describe how the identities passed into an individual verification step are verified. """ from __future__ import annotations import logging from abc import ABC, abstractmethod from typing import Protocol from cryptography.x509 import ( Certificate, ExtensionNotFound, ObjectIdentifier, OtherName, RFC822Name, SubjectAlternativeName, UniformResourceIdentifier, ) from pyasn1.codec.der.decoder import decode as der_decode from pyasn1.type.char import UTF8String from sigstore.errors import VerificationError _logger = logging.getLogger(__name__) # From: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md _OIDC_ISSUER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.1") _OIDC_GITHUB_WORKFLOW_TRIGGER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.2") _OIDC_GITHUB_WORKFLOW_SHA_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.3") _OIDC_GITHUB_WORKFLOW_NAME_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.4") _OIDC_GITHUB_WORKFLOW_REPOSITORY_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.5") _OIDC_GITHUB_WORKFLOW_REF_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.6") _OTHERNAME_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.7") _OIDC_ISSUER_V2_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.8") _OIDC_BUILD_SIGNER_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.9") _OIDC_BUILD_SIGNER_DIGEST_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.10") _OIDC_RUNNER_ENVIRONMENT_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.11") _OIDC_SOURCE_REPOSITORY_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.12") _OIDC_SOURCE_REPOSITORY_DIGEST_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.13") _OIDC_SOURCE_REPOSITORY_REF_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.14") _OIDC_SOURCE_REPOSITORY_IDENTIFIER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.15") _OIDC_SOURCE_REPOSITORY_OWNER_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.16") _OIDC_SOURCE_REPOSITORY_OWNER_IDENTIFIER_OID = ObjectIdentifier( "1.3.6.1.4.1.57264.1.17" ) _OIDC_BUILD_CONFIG_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.18") _OIDC_BUILD_CONFIG_DIGEST_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.19") _OIDC_BUILD_TRIGGER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.20") _OIDC_RUN_INVOCATION_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.21") _OIDC_SOURCE_REPOSITORY_VISIBILITY_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.22") class _SingleX509ExtPolicy(ABC): """ An ABC for verification policies that boil down to checking a single X.509 extension's value. """ oid: ObjectIdentifier """ The OID of the extension being checked. """ def __init__(self, value: str) -> None: """ Creates the new policy, with `value` as the expected value during verification. """ self._value = value def verify(self, cert: Certificate) -> None: """ Verify this policy against `cert`. Raises `VerificationError` on failure. """ try: ext = cert.extensions.get_extension_for_oid(self.oid).value except ExtensionNotFound: raise VerificationError( f"Certificate does not contain {self.__class__.__name__} " f"({self.oid.dotted_string}) extension" ) # NOTE(ww): mypy is confused by the `Extension[ExtensionType]` returned # by `get_extension_for_oid` above. ext_value = ext.value.decode() # type: ignore[attr-defined] if ext_value != self._value: raise VerificationError( f"Certificate's {self.__class__.__name__} does not match " f"(got '{ext_value}', expected '{self._value}')" ) class _SingleX509ExtPolicyV2(_SingleX509ExtPolicy): """ An base class for verification policies that boil down to checking a single X.509 extension's value, where the value is formatted as a DER-encoded string, the ASN.1 tag is UTF8String (0x0C) and the tag class is universal. """ def verify(self, cert: Certificate) -> None: """ Verify this policy against `cert`. Raises `VerificationError` on failure. """ try: ext = cert.extensions.get_extension_for_oid(self.oid).value except ExtensionNotFound: raise VerificationError( f"Certificate does not contain {self.__class__.__name__} " f"({self.oid.dotted_string}) extension" ) # NOTE(ww): mypy is confused by the `Extension[ExtensionType]` returned # by `get_extension_for_oid` above. ext_value = der_decode(ext.value, UTF8String)[0].decode() # type: ignore[attr-defined] if ext_value != self._value: raise VerificationError( f"Certificate's {self.__class__.__name__} does not match " f"(got {ext_value}, expected {self._value})" ) class OIDCIssuer(_SingleX509ExtPolicy): """ Verifies the certificate's OIDC issuer, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.1`. """ oid = _OIDC_ISSUER_OID class GitHubWorkflowTrigger(_SingleX509ExtPolicy): """ Verifies the certificate's GitHub Actions workflow trigger, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.2`. """ oid = _OIDC_GITHUB_WORKFLOW_TRIGGER_OID class GitHubWorkflowSHA(_SingleX509ExtPolicy): """ Verifies the certificate's GitHub Actions workflow commit SHA, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.3`. """ oid = _OIDC_GITHUB_WORKFLOW_SHA_OID class GitHubWorkflowName(_SingleX509ExtPolicy): """ Verifies the certificate's GitHub Actions workflow name, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.4`. """ oid = _OIDC_GITHUB_WORKFLOW_NAME_OID class GitHubWorkflowRepository(_SingleX509ExtPolicy): """ Verifies the certificate's GitHub Actions workflow repository, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.5`. """ oid = _OIDC_GITHUB_WORKFLOW_REPOSITORY_OID class GitHubWorkflowRef(_SingleX509ExtPolicy): """ Verifies the certificate's GitHub Actions workflow ref, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.6`. """ oid = _OIDC_GITHUB_WORKFLOW_REF_OID class OIDCIssuerV2(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC issuer, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.8`. The difference with `OIDCIssuer` is that the value for this extension is formatted to the RFC 5280 specification as a DER-encoded string. """ oid = _OIDC_ISSUER_V2_OID class OIDCBuildSignerURI(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Build Signer URI, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.9`. """ oid = _OIDC_BUILD_SIGNER_URI_OID class OIDCBuildSignerDigest(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Build Signer Digest, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.10`. """ oid = _OIDC_BUILD_SIGNER_DIGEST_OID class OIDCRunnerEnvironment(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Runner Environment, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.11`. """ oid = _OIDC_RUNNER_ENVIRONMENT_OID class OIDCSourceRepositoryURI(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Source Repository URI, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.12`. """ oid = _OIDC_SOURCE_REPOSITORY_URI_OID class OIDCSourceRepositoryDigest(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Source Repository Digest, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.13`. """ oid = _OIDC_SOURCE_REPOSITORY_DIGEST_OID class OIDCSourceRepositoryRef(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Source Repository Ref, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.14`. """ oid = _OIDC_SOURCE_REPOSITORY_REF_OID class OIDCSourceRepositoryIdentifier(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Source Repository Identifier, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.15`. """ oid = _OIDC_SOURCE_REPOSITORY_IDENTIFIER_OID class OIDCSourceRepositoryOwnerURI(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Source Repository Owner URI, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.16`. """ oid = _OIDC_SOURCE_REPOSITORY_OWNER_URI_OID class OIDCSourceRepositoryOwnerIdentifier(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Source Repository Owner Identifier, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.17`. """ oid = _OIDC_SOURCE_REPOSITORY_OWNER_IDENTIFIER_OID class OIDCBuildConfigURI(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Build Config URI, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.18`. """ oid = _OIDC_BUILD_CONFIG_URI_OID class OIDCBuildConfigDigest(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Build Config Digest, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.19`. """ oid = _OIDC_BUILD_CONFIG_DIGEST_OID class OIDCBuildTrigger(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Build Trigger, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.20`. """ oid = _OIDC_BUILD_TRIGGER_OID class OIDCRunInvocationURI(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Run Invocation URI, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.21`. """ oid = _OIDC_RUN_INVOCATION_URI_OID class OIDCSourceRepositoryVisibility(_SingleX509ExtPolicyV2): """ Verifies the certificate's OIDC Source Repository Visibility At Signing, identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.22`. """ oid = _OIDC_SOURCE_REPOSITORY_VISIBILITY_OID class VerificationPolicy(Protocol): """ A protocol type describing the interface that all verification policies conform to. """ @abstractmethod def verify(self, cert: Certificate) -> None: """ Verify the given `cert` against this policy, raising `VerificationError` on failure. """ raise NotImplementedError # pragma: no cover class AnyOf: """ The "any of" policy, corresponding to a logical OR between child policies. An empty list of child policies is considered trivially invalid. """ def __init__(self, children: list[VerificationPolicy]): """ Create a new `AnyOf`, with the given child policies. """ self._children = children def verify(self, cert: Certificate) -> None: """ Verify `cert` against the policy. Raises `VerificationError` on failure. """ for child in self._children: try: child.verify(cert) except VerificationError: pass else: return raise VerificationError(f"0 of {len(self._children)} policies succeeded") class AllOf: """ The "all of" policy, corresponding to a logical AND between child policies. An empty list of child policies is considered trivially invalid. """ def __init__(self, children: list[VerificationPolicy]): """ Create a new `AllOf`, with the given child policies. """ self._children = children def verify(self, cert: Certificate) -> None: """ Verify `cert` against the policy. """ # Without this, we'd consider empty lists of child policies trivially valid. # This is almost certainly not what the user wants and is a potential # source of API misuse, so we explicitly disallow it. if len(self._children) < 1: raise VerificationError("no child policies to verify") for child in self._children: child.verify(cert) class UnsafeNoOp: """ The "no-op" policy, corresponding to a no-op "verification". **This policy is fundamentally insecure. You cannot use it safely. It must not be used to verify any sort of certificate identity, because it cannot do so. Using this policy is equivalent to reducing the verification proof down to an integrity check against a completely untrusted and potentially attacker-created signature. It must only be used for testing purposes.** """ def verify(self, cert: Certificate) -> None: """ Verify `cert` against the policy. """ _logger.warning( "unsafe (no-op) verification policy used! no verification performed!" ) class Identity: """ Verifies the certificate's "identity", corresponding to the X.509v3 SAN. Identities can be verified modulo an OIDC issuer, to prevent an unexpected issuer from offering a particular identity. Supported SAN types include emails, URIs, and Sigstore-specific "other names". """ _issuer: OIDCIssuer | None def __init__(self, *, identity: str, issuer: str | None = None): """ Create a new `Identity`, with the given expected identity and issuer values. """ self._identity = identity if issuer: self._issuer = OIDCIssuer(issuer) else: self._issuer = None def verify(self, cert: Certificate) -> None: """ Verify `cert` against the policy. """ if self._issuer: self._issuer.verify(cert) # Build a set of all valid identities. san_ext = cert.extensions.get_extension_for_class(SubjectAlternativeName).value all_sans = set(san_ext.get_values_for_type(RFC822Name)) all_sans.update(san_ext.get_values_for_type(UniformResourceIdentifier)) all_sans.update( [ on.value.decode() for on in san_ext.get_values_for_type(OtherName) if on.type_id == _OTHERNAME_OID ] ) verified = self._identity in all_sans if not verified: raise VerificationError( f"Certificate's SANs do not match {self._identity}; actual SANs: {all_sans}" ) ================================================ FILE: sigstore/verify/verifier.py ================================================ # 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. """ Verification API machinery. """ from __future__ import annotations import base64 import logging from datetime import datetime, timezone from typing import cast import rekor_types from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage from cryptography.x509.oid import ExtendedKeyUsageOID from OpenSSL.crypto import ( X509, X509Store, X509StoreContext, X509StoreContextError, X509StoreFlags, ) from pydantic import ValidationError from rfc3161_client import TimeStampResponse, VerifierBuilder from rfc3161_client import VerificationError as Rfc3161VerificationError from sigstore_models.common import v1 from sigstore_models.rekor import v2 from sigstore import dsse from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._internal.sct import ( verify_sct, ) from sigstore._internal.timestamp import TimestampSource, TimestampVerificationResult from sigstore._internal.trust import KeyringPurpose from sigstore._utils import base64_encode_pem_cert, sha256_digest from sigstore.errors import CertValidationError, VerificationError from sigstore.hashes import Hashed from sigstore.models import Bundle, ClientTrustConfig, TrustedRoot from sigstore.verify.policy import VerificationPolicy _logger = logging.getLogger(__name__) # Limit the number of timestamps to prevent DoS # From https://github.com/sigstore/sigstore-go/blob/e92142f0734064ebf6001f188b7330a1212245fe/pkg/verify/tsa.go#L29 MAX_ALLOWED_TIMESTAMP: int = 32 # When verifying an entry, this threshold represents the minimum number of required # verified times to consider a signature valid. VERIFIED_TIME_THRESHOLD: int = 1 class Verifier: """ The primary API for verification operations. """ def __init__(self, *, trusted_root: TrustedRoot): """ Create a new `Verifier`. `trusted_root` is the `TrustedRoot` object containing the root of trust for the verification process. """ self._fulcio_certificate_chain: list[X509] = [ X509.from_cryptography(parent_cert) for parent_cert in trusted_root.get_fulcio_certs() ] self._trusted_root = trusted_root # this is an ugly hack needed for verifying "detached" materials # In reality we should be choosing the rekor instance based on the logid url = trusted_root._inner.tlogs[0].base_url self._rekor = RekorClient(url) @classmethod def production(cls, *, offline: bool = False) -> Verifier: """ Return a `Verifier` instance configured against Sigstore's production-level services. `offline` controls the Trusted Root refresh behavior: if `True`, the verifier uses the Trusted Root in the local TUF cache. If `False`, a TUF repository refresh is attempted. """ config = ClientTrustConfig.production(offline=offline) return cls( trusted_root=config.trusted_root, ) @classmethod def staging(cls, *, offline: bool = False) -> Verifier: """ Return a `Verifier` instance configured against Sigstore's staging-level services. `offline` controls the Trusted Root refresh behavior: if `True`, the verifier uses the Trusted Root in the local TUF cache. If `False`, a TUF repository refresh is attempted. """ config = ClientTrustConfig.staging(offline=offline) return cls( trusted_root=config.trusted_root, ) def _verify_signed_timestamp( self, timestamp_response: TimeStampResponse, message: bytes ) -> TimestampVerificationResult | None: """ Verify a Signed Timestamp using the TSA provided by the Trusted Root. """ cert_authorities = self._trusted_root.get_timestamp_authorities() for certificate_authority in cert_authorities: certificates = certificate_authority.certificates(allow_expired=True) # We expect at least a signing cert and a root cert but there may be intermediates if len(certificates) < 2: _logger.debug("Unable to verify Timestamp: cert chain is incomplete") continue builder = ( VerifierBuilder() .tsa_certificate(certificates[0]) .add_root_certificate(certificates[-1]) ) for certificate in certificates[1:-1]: builder = builder.add_intermediate_certificate(certificate) verifier = builder.build() try: verifier.verify_message(timestamp_response, message) except Rfc3161VerificationError: _logger.debug("Unable to verify Timestamp with CA.", exc_info=True) continue if ( certificate_authority.validity_period_start <= timestamp_response.tst_info.gen_time ) and ( not certificate_authority.validity_period_end or timestamp_response.tst_info.gen_time < certificate_authority.validity_period_end ): return TimestampVerificationResult( source=TimestampSource.TIMESTAMP_AUTHORITY, time=timestamp_response.tst_info.gen_time, ) _logger.debug("Unable to verify Timestamp because not in CA time range.") return None def _verify_timestamp_authority( self, bundle: Bundle ) -> list[TimestampVerificationResult]: """ Verify that the given bundle has been timestamped by a trusted timestamp authority and that the timestamp is valid. Returns the number of valid signed timestamp in the bundle. """ timestamp_responses = [] if ( timestamp_verification_data := bundle.verification_material.timestamp_verification_data ): timestamp_responses = timestamp_verification_data.rfc3161_timestamps if len(timestamp_responses) > MAX_ALLOWED_TIMESTAMP: msg = f"too many signed timestamp: {len(timestamp_responses)} > {MAX_ALLOWED_TIMESTAMP}" raise VerificationError(msg) if len(set(timestamp_responses)) != len(timestamp_responses): msg = "duplicate timestamp found" raise VerificationError(msg) verified_timestamps = [ result for tsr in timestamp_responses if (result := self._verify_signed_timestamp(tsr, bundle.signature)) ] return verified_timestamps def _establish_time(self, bundle: Bundle) -> list[TimestampVerificationResult]: """ Establish the time for bundle verification. This method uses timestamps from two possible sources: 1. RFC3161 signed timestamps from a Timestamping Authority (TSA) 2. Transparency Log timestamps """ verified_timestamps = [] # If a timestamp from the timestamping service is available, the Verifier MUST # perform path validation using the timestamp from the Timestamping Service. if bundle.verification_material.timestamp_verification_data: if not self._trusted_root.get_timestamp_authorities(): msg = ( "no Timestamp Authorities have been provided to validate this " "bundle but it contains a signed timestamp" ) raise VerificationError(msg) timestamp_from_tsa = self._verify_timestamp_authority(bundle) verified_timestamps.extend(timestamp_from_tsa) # If a timestamp from the Transparency Service is available, the Verifier MUST # perform path validation using the timestamp from the Transparency Service. # NOTE: We only include this timestamp if it's accompanied by an inclusion # promise that cryptographically binds it. We verify the inclusion promise # itself later, as part of log entry verification. if ( timestamp := bundle.log_entry._inner.integrated_time ) and bundle.log_entry._inner.inclusion_promise: kv = bundle.log_entry._inner.kind_version if not (kv.kind in ["dsse", "hashedrekord"] and kv.version == "0.0.1"): raise VerificationError( "Integrated time only supported for dsse/hashedrekord 0.0.1 types" ) verified_timestamps.append( TimestampVerificationResult( source=TimestampSource.TRANSPARENCY_SERVICE, time=datetime.fromtimestamp(timestamp, tz=timezone.utc), ) ) return verified_timestamps def _verify_chain_at_time( self, certificate: X509, timestamp_result: TimestampVerificationResult ) -> list[X509]: """ Verify the validity of the certificate chain at the given time. Raises a VerificationError if the chain can't be built or be verified. """ # NOTE: The `X509Store` object cannot have its time reset once the `set_time` # method been called on it. To get around this, we construct a new one in each # call. store = X509Store() # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN # would be strictly more conformant of OpenSSL, but we currently # *want* the "long" chain behavior of performing path validation # down to a self-signed root. store.set_flags(X509StoreFlags.X509_STRICT) for parent_cert_ossl in self._fulcio_certificate_chain: store.add_cert(parent_cert_ossl) store.set_time(timestamp_result.time) store_ctx = X509StoreContext(store, certificate) try: # get_verified_chain returns the full chain including the end-entity certificate # and chain should contain only CA certificates return store_ctx.get_verified_chain()[1:] except X509StoreContextError as e: raise CertValidationError( f"failed to build timestamp certificate chain: {e}" ) def _verify_common_signing_cert( self, bundle: Bundle, policy: VerificationPolicy ) -> None: """ Performs the signing certificate verification steps that are shared between `verify_dsse` and `verify_artifact`. Raises `VerificationError` on all failures. """ # In order to verify an artifact, we need to achieve the following: # # 0. Establish a time for the signature. # 1. Verify that the signing certificate chains to the root of trust # and is valid at the time of signing. # 2. Verify the signing certificate's SCT. # 3. Verify that the signing certificate conforms to the Sigstore # X.509 profile as well as the passed-in `VerificationPolicy`. # 4. Verify the inclusion proof and signed checkpoint for the log # entry. # 5. Verify the inclusion promise for the log entry, if present. # 6. Verify the timely insertion of the log entry against the validity # period for the signing certificate. # 7. Verify the signature and input against the signing certificate's # public key. # 8. Verify the transparency log entry's consistency against the other # materials, to prevent variants of CVE-2022-36056. # # This method performs steps (0) through (6) above. Its caller # MUST perform steps (7) and (8) separately, since they vary based on # the kind of verification being performed (i.e. hashedrekord, DSSE, etc.) cert = bundle.signing_certificate # NOTE: The `X509Store` object currently cannot have its time reset once the `set_time` # method been called on it. To get around this, we construct a new one for every `verify` # call. store = X509Store() # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN # would be strictly more conformant of OpenSSL, but we currently # *want* the "long" chain behavior of performing path validation # down to a self-signed root. store.set_flags(X509StoreFlags.X509_STRICT) for parent_cert_ossl in self._fulcio_certificate_chain: store.add_cert(parent_cert_ossl) # (0): Establishing a Time for the Signature # First, establish verified times for the signature. This is required to # validate the certificate chain, so this step comes first. # These include TSA timestamps and (in the case of rekor v1 entries) # rekor log integrated time. verified_timestamps = self._establish_time(bundle) if len(verified_timestamps) < VERIFIED_TIME_THRESHOLD: raise VerificationError("not enough sources of verified time") # (1): verify that the signing certificate is signed by the root # certificate and that the signing certificate was valid at the # time of signing. cert_ossl = X509.from_cryptography(cert) chain: list[X509] = [] for vts in verified_timestamps: chain = self._verify_chain_at_time(cert_ossl, vts) # (2): verify the signing certificate's SCT. try: verify_sct( cert, [parent_cert.to_cryptography() for parent_cert in chain], self._trusted_root.ct_keyring(KeyringPurpose.VERIFY), ) except VerificationError as e: raise VerificationError(f"failed to verify SCT on signing certificate: {e}") # (3): verify the signing certificate against the Sigstore # X.509 profile and verify against the given `VerificationPolicy`. usage_ext = cert.extensions.get_extension_for_class(KeyUsage) if not usage_ext.value.digital_signature: raise VerificationError("Key usage is not of type `digital signature`") extended_usage_ext = cert.extensions.get_extension_for_class(ExtendedKeyUsage) if ExtendedKeyUsageOID.CODE_SIGNING not in extended_usage_ext.value: raise VerificationError("Extended usage does not contain `code signing`") policy.verify(cert) _logger.debug("Successfully verified signing certificate validity...") # (4): verify the inclusion proof and signed checkpoint for the # log entry. # (5): verify the inclusion promise for the log entry, if present. entry = bundle.log_entry try: entry._verify(self._trusted_root.rekor_keyring(KeyringPurpose.VERIFY)) except VerificationError as exc: raise VerificationError(f"invalid log entry: {exc}") # (6): verify our established times (timestamps or the log integration time) are # within signing certificate validity period. for vts in verified_timestamps: if not ( bundle.signing_certificate.not_valid_before_utc <= vts.time <= bundle.signing_certificate.not_valid_after_utc ): raise VerificationError( f"invalid signing cert: expired at time of signing, time via {vts}" ) def verify_dsse( self, bundle: Bundle, policy: VerificationPolicy ) -> tuple[str, bytes]: """ Verifies an bundle's DSSE envelope, returning the encapsulated payload and its content type. This method is only for DSSE-enveloped payloads. To verify an arbitrary input against a bundle, use the `verify_artifact` method. `bundle` is the Sigstore `Bundle` to both verify and verify against. `policy` is the `VerificationPolicy` to verify against. Returns a tuple of `(type, payload)`, where `type` is the payload's type as encoded in the DSSE envelope and `payload` is the raw `bytes` of the payload. No validation of either `type` or `payload` is performed; users of this API **must** assert that `type` is known to them before proceeding to handle `payload` in an application-dependent manner. """ # (1) through (6) are performed by `_verify_common_signing_cert`. self._verify_common_signing_cert(bundle, policy) # (7): verify the bundle's signature and DSSE envelope against the # signing certificate's public key. envelope = bundle._dsse_envelope if envelope is None: raise VerificationError( "cannot perform DSSE verification on a bundle without a DSSE envelope" ) signing_key = bundle.signing_certificate.public_key() signing_key = cast(ec.EllipticCurvePublicKey, signing_key) dsse._verify(signing_key, envelope) # (8): verify the consistency of the log entry's body against # the other bundle materials. # NOTE: This is very slightly weaker than the consistency check # for hashedrekord entries, due to how inclusion is recorded for DSSE: # the included entry for DSSE includes an envelope hash that we # *cannot* verify, since the envelope is uncanonicalized JSON. # Instead, we manually pick apart the entry body below and verify # the parts we can (namely the payload hash and signature list). entry = bundle.log_entry if entry._inner.kind_version.kind != "dsse": raise VerificationError( f"Expected entry type dsse, got {entry._inner.kind_version.kind}" ) if entry._inner.kind_version.version == "0.0.2": _validate_dsse_v002_entry_body(bundle) elif entry._inner.kind_version.version == "0.0.1": _validate_dsse_v001_entry_body(bundle) else: raise VerificationError( f"Unsupported dsse version {entry._inner.kind_version.version}" ) return (envelope._inner.payload_type, envelope._inner.payload) def verify_artifact( self, input_: bytes | Hashed, bundle: Bundle, policy: VerificationPolicy, ) -> None: """ Public API for verifying. `input_` is the input to verify, either as a buffer of contents or as a prehashed `Hashed` object. `bundle` is the Sigstore `Bundle` to verify against. `policy` is the `VerificationPolicy` to verify against. On failure, this method raises `VerificationError`. """ # (1) through (6) are performed by `_verify_common_signing_cert`. self._verify_common_signing_cert(bundle, policy) hashed_input = sha256_digest(input_) bundle_signature = bundle._inner.message_signature if bundle_signature is None: raise VerificationError("Missing bundle message signature") # signature is verified over input digest, but if the bundle documents the digest we still # want to ensure it matches the input digest: if ( bundle_signature.message_digest is not None and hashed_input.digest != bundle_signature.message_digest.digest ): raise VerificationError("Bundle message digest mismatch") # (7): verify that the signature was signed by the public key in the signing certificate. try: signing_key = bundle.signing_certificate.public_key() signing_key = cast(ec.EllipticCurvePublicKey, signing_key) signing_key.verify( bundle_signature.signature, hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed()), ) except InvalidSignature: raise VerificationError("Signature is invalid for input") _logger.debug("Successfully verified signature...") # (8): verify the consistency of the log entry's body against # the other bundle materials (and input being verified). entry = bundle.log_entry if entry._inner.kind_version.kind != "hashedrekord": raise VerificationError( f"Expected entry type hashedrekord, got {entry._inner.kind_version.kind}" ) if entry._inner.kind_version.version == "0.0.2": _validate_hashedrekord_v002_entry_body(bundle, hashed_input) elif entry._inner.kind_version.version == "0.0.1": _validate_hashedrekord_v001_entry_body(bundle, hashed_input) else: raise VerificationError( f"Unsupported hashedrekord version {entry._inner.kind_version.version}" ) def _validate_dsse_v001_entry_body(bundle: Bundle) -> None: """ Validate the Entry body for dsse v001. """ entry = bundle.log_entry envelope = bundle._dsse_envelope if envelope is None: raise VerificationError( "cannot perform DSSE verification on a bundle without a DSSE envelope" ) try: entry_body = rekor_types.Dsse.model_validate_json( entry._inner.canonicalized_body ) except ValidationError as exc: raise VerificationError(f"invalid DSSE log entry: {exc}") payload_hash = sha256_digest(envelope._inner.payload).digest.hex() if ( entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] != rekor_types.dsse.Algorithm.SHA256 ): raise VerificationError("expected SHA256 payload hash in DSSE log entry") if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] raise VerificationError("log entry payload hash does not match bundle") # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, # but we handle them just in case the signer has somehow produced multiple # signatures for their envelope with the same signing key. signatures = [ rekor_types.dsse.Signature( signature=base64.b64encode(signature.sig).decode(), verifier=base64_encode_pem_cert(bundle.signing_certificate), ) for signature in envelope._inner.signatures ] if signatures != entry_body.spec.root.signatures: raise VerificationError("log entry signatures do not match bundle") def _validate_dsse_v002_entry_body(bundle: Bundle) -> None: """ Validate Entry body for dsse v002. """ entry = bundle.log_entry envelope = bundle._dsse_envelope if envelope is None: raise VerificationError( "cannot perform DSSE verification on a bundle without a DSSE envelope" ) try: v2_body = v2.entry.Entry.from_json(entry._inner.canonicalized_body) except ValidationError as exc: raise VerificationError(f"invalid DSSE log entry: {exc}") if v2_body.spec.dsse_v002 is None: raise VerificationError("invalid DSSE log entry: missing dsse_v002 field") if v2_body.spec.dsse_v002.payload_hash.algorithm != v1.HashAlgorithm.SHA2_256: raise VerificationError("expected SHA256 hash in DSSE entry") digest = sha256_digest(envelope._inner.payload).digest if v2_body.spec.dsse_v002.payload_hash.digest != digest: raise VerificationError("DSSE entry payload hash does not match bundle") v2_signatures = [ v2.verifier.Signature( content=base64.b64encode(signature.sig), verifier=_v2_verifier_from_certificate(bundle.signing_certificate), ) for signature in envelope._inner.signatures ] if v2_signatures != v2_body.spec.dsse_v002.signatures: raise VerificationError("log entry signatures do not match bundle") def _validate_hashedrekord_v001_entry_body( bundle: Bundle, hashed_input: Hashed ) -> None: """ Validate the Entry body for hashedrekord v001. """ entry = bundle.log_entry expected_body = _hashedrekord_from_parts( bundle.signing_certificate, bundle._inner.message_signature.signature, # type: ignore[union-attr] hashed_input, ) actual_body = rekor_types.Hashedrekord.model_validate_json( entry._inner.canonicalized_body ) if expected_body != actual_body: raise VerificationError( "transparency log entry is inconsistent with other materials" ) def _validate_hashedrekord_v002_entry_body( bundle: Bundle, hashed_input: Hashed ) -> None: """ Validate Entry body for hashedrekord v002. """ entry = bundle.log_entry if bundle._inner.message_signature is None: raise VerificationError( "invalid hashedrekord log entry: missing message signature" ) v2_expected_body = v2.entry.Entry( kind=entry._inner.kind_version.kind, api_version=entry._inner.kind_version.version, spec=v2.entry.Spec( hashed_rekord_v002=v2.hashedrekord.HashedRekordLogEntryV002( data=v1.HashOutput( algorithm=hashed_input.algorithm, digest=base64.b64encode(hashed_input.digest), ), signature=v2.verifier.Signature( content=base64.b64encode(bundle._inner.message_signature.signature), verifier=_v2_verifier_from_certificate(bundle.signing_certificate), ), ) ), ) v2_actual_body = v2.entry.Entry.from_json(entry._inner.canonicalized_body) if v2_expected_body != v2_actual_body: raise VerificationError( "transparency log entry is inconsistent with other materials" ) def _v2_verifier_from_certificate(certificate: Certificate) -> v2.verifier.Verifier: """ Return a Rekor v2 Verifier for the signing certificate. This method decides which signature algorithms are supported for verification (in a rekor v2 entry), see https://github.com/sigstore/architecture-docs/blob/main/algorithm-registry.md. Note that actual signature verification happens in verify_artifact() and verify_dsse(): New keytypes need to be added here and in those methods. """ public_key = certificate.public_key() if isinstance(public_key, ec.EllipticCurvePublicKey): if isinstance(public_key.curve, ec.SECP256R1): key_details = v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256 elif isinstance(public_key.curve, ec.SECP384R1): key_details = v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384 elif isinstance(public_key.curve, ec.SECP521R1): key_details = v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512 else: raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") else: raise ValueError(f"Unsupported public key type: {type(public_key)}") return v2.verifier.Verifier( x509_certificate=v1.X509Certificate( raw_bytes=base64.b64encode( certificate.public_bytes(encoding=serialization.Encoding.DER) ) ), key_details=key_details, ) ================================================ FILE: test/assets/a.dsse.staging-rekor-v2.txt ================================================ DO NOT MODIFY ME! this is "a.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json ================================================ {"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": {"certificate": {"rawBytes": "MIIDBDCCAoqgAwIBAgIUYlZafqye+P/bWSMSdvxrr7y+NUEwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNjA5MjEwNjI1WhcNMjUwNjA5MjExNjI1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwDj9XB2rrkUTaCgPE3OGPJ+176EZM3u2SK2XLKoMUQn79zywhocahVPybzn/6nMkWkew8SFaDhkL4PCAENNzcqOCAakwggGlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUQ/OiAAk5AAqjN5apYfVwt/M4S5UwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwWQYDVR0RAQH/BE8wTYFLaW5zZWN1cmUtY2xvdWR0b3Atc2hhcmVkLXVzZXJAY2xvdWR0b3AtcHJvZC11cy1lYXN0LmlhbS5nc2VydmljZWFjY291bnQuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABl1aEEo4AAAQDAEcwRQIhAJzFA8xqE8owuQqk9ao7NLQy/YoTsy23A+ZU3cdL+MM1AiAZyN3FSWf13Fl3oL+P5jAvv0xRyqGrWEyZJw4KO7XhnDAKBggqhkjOPQQDAwNoADBlAjA9OgkRsqwLbt59TB0Jb15NBBQiaNBRRqUdo2FuSrvEWWDnnynmqo0GygnbCmz2CJwCMQDFCWJExAUGX7v5UQUzDz1pc1b0WvX1wAP2fhbgir2yZZRcsr4OdWz31arOo6USvVI="}, "tlogEntries": [{"logIndex": "689", "logId": {"keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck="}, "kindVersion": {"kind": "dsse", "version": "0.0.2"}, "inclusionProof": {"logIndex": "689", "rootHash": "VLopDAB81ENEy7SM2Oe4gxf026TulneLw22pUPlt0qE=", "treeSize": "690", "hashes": ["7G2mWiDIVCMp4cUCF9+qqADG/ICLRt3I2I9nqIWaKnA=", "/Fm4+swicRuu0gv27PWsZ2C1hw3IbCcatPnSV6oTbOw=", "9AF3UpKoSTEa5MS8BHGJxKHH9zVkJgn29s03k14ZtdI=", "QMesRTEZdIgthOEinYE/9J7wGv+VmArDZTICj9POmhY=", "UNUMG62rMwoqCqFKknh4R5Ubkf5Z6dj+Pk0m/1xu8uo="], "checkpoint": {"envelope": "log2025-alpha1.rekor.sigstage.dev\n690\nVLopDAB81ENEy7SM2Oe4gxf026TulneLw22pUPlt0qE=\n\n\u2014 log2025-alpha1.rekor.sigstage.dev 8w1amfdsl47Li2mk9esQ1K+vF9tg8WCLlNKBcoVTzrHr4howD6z2171ij8XW6d48AUEoV4PK1DDz5jHUlCQ98okwLQw=\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZHNzZVYwMDIiOnsicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiI0a2QxR3VyKzFmZE1wMHVBZFJyQnBQYTZONXB3OWx0b25pZXdlekg4MmhvPSJ9LCJzaWduYXR1cmVzIjpbeyJjb250ZW50IjoiTUVZQ0lRQ3F6dEJCTXpiYmU3alN6NXFQOE93U3hKWDBFb0VTSGg5d21uRXljUzd3S3dJaEFMd1BIaWt0b2dRY3greFZMWEhsSU56dTI1clRTNW5YRkJ3OEtxcXp5OGZkIiwidmVyaWZpZXIiOnsia2V5RGV0YWlscyI6IlBLSVhfRUNEU0FfUDI1Nl9TSEFfMjU2IiwieDUwOUNlcnRpZmljYXRlIjp7InJhd0J5dGVzIjoiTUlJREJEQ0NBb3FnQXdJQkFnSVVZbFphZnF5ZStQL2JXU01TZHZ4cnI3eStOVUV3Q2dZSUtvWkl6ajBFQXdNd056RVZNQk1HQTFVRUNoTU1jMmxuYzNSdmNtVXVaR1YyTVI0d0hBWURWUVFERXhWemFXZHpkRzl5WlMxcGJuUmxjbTFsWkdsaGRHVXdIaGNOTWpVd05qQTVNakV3TmpJMVdoY05NalV3TmpBNU1qRXhOakkxV2pBQU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXdEajlYQjJycmtVVGFDZ1BFM09HUEorMTc2RVpNM3UyU0syWExLb01VUW43OXp5d2hvY2FoVlB5YnpuLzZuTWtXa2V3OFNGYURoa0w0UENBRU5OemNxT0NBYWt3Z2dHbE1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVRL09pQUFrNUFBcWpONWFwWWZWd3QvTTRTNVV3SHdZRFZSMGpCQmd3Rm9BVWNZWXdwaFI4WW0vNTk5YjBCUnAvWC8vcmI2d3dXUVlEVlIwUkFRSC9CRTh3VFlGTGFXNXpaV04xY21VdFkyeHZkV1IwYjNBdGMyaGhjbVZrTFhWelpYSkFZMnh2ZFdSMGIzQXRjSEp2WkMxMWN5MWxZWE4wTG1saGJTNW5jMlZ5ZG1salpXRmpZMjkxYm5RdVkyOXRNQ2tHQ2lzR0FRUUJnNzh3QVFFRUcyaDBkSEJ6T2k4dllXTmpiM1Z1ZEhNdVoyOXZaMnhsTG1OdmJUQXJCZ29yQmdFRUFZTy9NQUVJQkIwTUcyaDBkSEJ6T2k4dllXTmpiM1Z1ZEhNdVoyOXZaMnhsTG1OdmJUQ0JpZ1lLS3dZQkJBSFdlUUlFQWdSOEJIb0FlQUIyQUNzd3ZOeG9pTW5pNGRnbUtWNTBIMGc1TVpZQzhwd3p5MTVEUVA2eXJJWjZBQUFCbDFhRUVvNEFBQVFEQUVjd1JRSWhBSnpGQTh4cUU4b3d1UXFrOWFvN05MUXkvWW9Uc3kyM0ErWlUzY2RMK01NMUFpQVp5TjNGU1dmMTNGbDNvTCtQNWpBdnYweFJ5cUdyV0V5Wkp3NEtPN1hobkRBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpBOU9na1JzcXdMYnQ1OVRCMEpiMTVOQkJRaWFOQlJScVVkbzJGdVNydkVXV0RubnlubXFvMEd5Z25iQ216MkNKd0NNUURGQ1dKRXhBVUdYN3Y1VVFVekR6MXBjMWIwV3ZYMXdBUDJmaGJnaXIyeVpaUmNzcjRPZFd6MzFhck9vNlVTdlZJPSJ9fX1dfX19"}], "timestampVerificationData": {"rfc3161Timestamps": [{"signedTimestamp": "MIIE5zADAgEAMIIE3gYJKoZIhvcNAQcCoIIEzzCCBMsCAQMxDTALBglghkgBZQMEAgEwgcEGCyqGSIb3DQEJEAEEoIGxBIGuMIGrAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg7mKrZuedCow8ht74HmPFNT7ZP18+JAF/WDRwwOFuzn8CFBKaF0PyLXni4RkH6K+ZuzF9x2JcGA8yMDI1MDYwOTIxMDYyOFowAwIBAQIIWJ9Fv2Y6K7CgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoIICEzCCAg8wggGWoAMCAQICFAo1oQZh1eJBc8aJlqfyffJ+A3ynMAoGCCqGSM49BAMDMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwHhcNMjUwMzI4MDkxNDA2WhcNMzUwMzI2MDgxNDA2WjAuMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxFTATBgNVBAMTDHNpZ3N0b3JlLXRzYTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMdb+Rdx6Q/XoB7pJ6QRZUc+0AUQybuGnlc7fcyS0WNJb5sdZRe1gTNnPQDfGRj0LJg6h5STdkf+/kcS5L5S85HNfSDsd/Le5hhhHAe2oFA3Qhfyst0Uy0itF6P9AIB0HaNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBSo/GT2KN4u5jtzT1SMUsThnN1TpTAfBgNVHSMEGDAWgBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAwNnADBkAjBEr1UuhhrRd9/idfU38BDViV40b+ItPx0BcC1EpF+k31e4NJxvFZ6jRyS7xKQLTo0CMFA97ssE16K0D9Q4G1dPaxfWHp/ghKrP4hKYniVj7LdvNEkjmeTWvncj1ZPf/EhZOjGCAdowggHWAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUCjWhBmHV4kFzxomWp/J98n4DfKcwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUwNjA5MjEwNjI4WjAvBgkqhkiG9w0BCQQxIgQgm3w3T24hj0XJHfurAzfPAUM+UpN9mOfHY9jwsQe6eYkwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIAb0/+BH/rNZmbczsNejI1Ac/BjkwDNmqEXXdTbnSydEMFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFAo1oQZh1eJBc8aJlqfyffJ+A3ynMAoGCCqGSM49BAMCBGYwZAIwJQ/ArYnYtKS38pLXrZ1A/CT1VGgDRUoSkslIGKlHU98qwoWUjjgmmdbeYakSqfENAjABbYaUoMwznhyQd8CKMo7f092Z3Plwa/enOQqgmyu1dAPpmD8rYr2VEjVEGKcvVoY="}]}}, "dsseEnvelope": {"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiZTI0OGE1ZGI0OTMzZGJhNjU3ODIwMDIzOGM5MWE1N2Y1ZTY1YjkyNWI3MzA1MGFlNzg2OTMzNDY4YjdhYzEwMSJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL3RhZ3MvMS4yMS4wIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9vY3RvLW9yZy9vY3RvLXJlcG8iLCJwYXRoIjoiLmdpdGh1Yi93b3JrZmxvd3MvY2kueWFtbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiIwMDAwMDAwMDAiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiMDAwMDAwMCIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL29jdG8tb3JnL29jdG8tcmVwb0ByZWZzL3RhZ3MvMS4yMS4wIiwiZGlnZXN0Ijp7ImdpdENvbW1pdCI6IjFhYzkzY2UyMWVlNTI2YjM2ZmQxNTRiOTA1OGQ5N2RmYWE0MjRjNTAifX1dfSwicnVuRGV0YWlscyI6eyJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly9naXRodWIuY29tL29jdG8tb3JnL29jdG8tcmVwby8uZ2l0aHViL3dvcmtmbG93cy9kb2NrZXIueWFtbEByZWZzL2hlYWRzL2RldmVsb3BtZW50In0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9vY3RvLW9yZy9vY3RvLXJlcG8vYWN0aW9ucy9ydW5zLzEwMzEzOTgzMjE4L2F0dGVtcHRzLzIifX19fQ==", "payloadType": "application/vnd.in-toto+json", "signatures": [{"sig": "MEYCIQCqztBBMzbbe7jSz5qP8OwSxJX0EoESHh9wmnEycS7wKwIhALwPHiktogQcx+xVLXHlINzu25rTS5nXFBw8Kqqzy8fd"}]}} ================================================ FILE: test/assets/a.txt ================================================ DO NOT MODIFY ME! this is "a.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/a.txt.crt ================================================ -----BEGIN CERTIFICATE----- MIIEfjCCBAWgAwIBAgIUf/SsSCcPgO7o0yKoONG3Vz/RdzIwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjIwNzI4MTcwNDIxWhcNMjIwNzI4MTcxNDIxWjAAMHYwEAYH KoZIzj0CAQYFK4EEACIDYgAEiWxhv1x6hf4JJjbH8RSPxMs3DW4tLlQpbBOuVLxQ sUcxsA2mIW5N3O91Vikum0xT5NFsve8bH1vqOCuSdJItW0uesGJtJRB0aCQUnweC AWgTFcgObyYZdqfOOgYBp59Qo4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBQdR6DA2mG5awJ5IeGQ/KwqhC8teTAf BgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3 aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo dWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb fBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYJFxDEtAAAEAQIAZtah 4QQqmJIzhkoDb79Q1NOUMreaes/FxxkHAqPrVbLA0aSnA3NEwScd7P8PgwbRiFGz LILsACKHBlTmlzVB8s6qTtDrtjP8JHV7NSbxa84QSL0XcHUhMMVSGNvi5gr5dSom TJsrfxJTX1+uLRDvxhqTdGoPIl9/J9ekTMQ/C8WNlSwAJbQDsAFG4PZhu3M6haH3 cH7l0VCDbewq42axDNlwbY7/jw9bvHrkU+PjSmRBOnyUuGJocHGDywqYA6vQLoO3 /UcSWFQ+/QFHxGVC4f6SrM2c+GCPBTLPUVUAJEwi0U6OnB7d6ObLsMEy0vY56GPJ TGSJtUhxMFp/zDhMdBeviRDX1tLpT5rjP34F1Iee3KMZh7tEY45NlcIhdCJ8bAbt j+X7YVI15jrpX3XVBNilTgvAZy0/c4ZBjRcr44GwbHQvYE8N/9WEIf6d8I+9/d6S mRZJVNEcHEA+bpWGT/S7nzgxrtusFejR5JxxPIes8AVsFKNkCq0BkxiwRWIrIEEJ 7TzdVZdZFc8OWthKlkVuylrbg2BJfARx7CMnywuj/gx23lf9B9IkgDRptPwJQCJm +AtNujk0BUTjh7VazWK+FBUqHDciZQEGDEksy24zMW+BxZwLUvQXpXUnKT9tzMLm YI7x0MeDKSd+lhi36Z5EJHyPGB0KPZ8KNRV49WIwCgYIKoZIzj0EAwMDZwAwZAIw a/88UepYCuhkuUtwwL4WGsvEO4fN+07Togp9ksspYhpDvgEZSrn/oEwx7cNRbl7e AjBk/QR+Nif+U8CfM9bqs9SEww+KEykj+uyAud/C/qt97HsI7j7I2ECuy3SJL6vW l7I= -----END CERTIFICATE----- ================================================ FILE: test/assets/a.txt.sig ================================================ MGUCMQDVGmInKk9wYEfCmnp+kPnLYM/P5B9FXR8Ec7AoLRrq+qExIWS9gcg0GPPYbFkqX7gCMAsGbuVHKJedWNF6vnV4J+3p8u8MhKvBTP+gBVeSZU1CuvULwDfU15EDEwgitIBgiA== ================================================ FILE: test/assets/b.txt ================================================ DO NOT MODIFY ME! this is "b.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/b.txt.crt ================================================ -----BEGIN CERTIFICATE----- MIIEfzCCBAWgAwIBAgIUXTEFeZc82/onXSK7L3gOx5ZryQYwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjIwNzI4MTcwNDIyWhcNMjIwNzI4MTcxNDIyWjAAMHYwEAYH KoZIzj0CAQYFK4EEACIDYgAE7fIULI0jGzRxDm/UhtOK2aYvsBkbc/J91AwXWRtu KEy1rPdf7MhP/IFyqQcvc9mBf2/tjBZEdgDDAZ7StmV9BHE3MgsYhkImsH4938mY EYHf6hzEsKkdiLjtHOcvAKX6o4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBQ7bzu/2AekVQ/lsYw3MCxIw+WXzDAf BgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3 aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo dWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb fBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYJFxDShAAAEAQIAsI37 okHyutfCBZKc0IAP8SgutX1wtA8cKfieySdpkWXnCqeiMjBpseEMaYIJPdpa3dwd Al1uLA5R24a9Oi6BDPJGFyZSrvRRtTz0oHKLrjVx0eDrL89KBb0Y554dkQH+XYS9 PgZqOAUq1cubDJ1VTH8EEnhU7BSx4i8/Z1jZx1DFNIde+H+esWRmuOQmlk7ViT5W bir99BdvzfeBHtXqlSVygVfJgiJpJDwl85hBZ7ayKvuArN5bl6mISvfvt/W1fH+B syn45LcjMqfRO+AwMP3Payv5/KbLV2WL9rfCkqCaR5Q94PHlh8KOAORVilay67kw g7+FvWGEHzYnk5iEV5/0gjtYwG1EnxsBMxNRtVVOhCLcQs3nfntYz794yvhd1S94 zH0QVo3przPEU40hFjU+cOdNEnVJ0DkspoKVtM9fOrJbL9XXqVLCnUKQBRfwizNZ 8GFEMi9VJ1lX1rybnP/SWWwiWsRTufWZ8ogpESUvSk3L/+XwFjuEy2gaWczODLAu Plj6f4YnHqAQxshf6Km7gUTodRw8yjXtYPPH1yxSZNJ1ivOUQdCS+I218SHloqsF DtHraHb68cRKSQXIsG8a1GGmlZ6ZOTpjFIq62qcRbrPPzTk1du5b0axEIaPHKPOs BwwKHrnZuVXE4yfwYXtciFgp8fsD8b8vBLJYH00wCgYIKoZIzj0EAwMDaAAwZQIx APr8q3OZd7zY78xfVpbusI1Vg7tvwOUYFrAHWMc0n5QIrhwfUm7/ZTPKyzVB2An9 2AIwS/qcggfuupePvsM7pHXXiDNexzTZBI9LP7evpXP32CM5cqebAn+kQFtxJ4jd afT2 -----END CERTIFICATE----- ================================================ FILE: test/assets/b.txt.sig ================================================ MGYCMQCI3sfFtuJ+nyZEhK7HrJNE9OczNsXAJDOoE25rjLMb1sy8uSRdAEz9FORDSW9g6OsCMQCbroOxfpnr77LkvVZqbdRvnAaa3ZJBWXSnz1EiYnJ3OWBWp+699o9b8u0AxiPnofI= ================================================ FILE: test/assets/bad.txt ================================================ DO NOT MODIFY ME! this is "bad.txt", a fiddled with input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/bad.txt.crt ================================================ -----BEGIN CERTIFICATE----- MIIEfjCCBAWgAwIBAgIUf/SsSCcPgO7o0yKoONG3Vz/RdzIwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjIwNzI4MTcwNDIxWhcNMjIwNzI4MTcxNDIxWjAAMHYwEAYH KoZIzj0CAQYFK4EEACIDYgAEiWxhv1x6hf4JJjbH8RSPxMs3DW4tLlQpbBOuVLxQ sUcxsA2mIW5N3O91Vikum0xT5NFsve8bH1vqOCuSdJItW0uesGJtJRB0aCQUnweC AWgTFcgObyYZdqfOOgYBp59Qo4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBQdR6DA2mG5awJ5IeGQ/KwqhC8teTAf BgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3 aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo dWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb fBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYJFxDEtAAAEAQIAZtah 4QQqmJIzhkoDb79Q1NOUMreaes/FxxkHAqPrVbLA0aSnA3NEwScd7P8PgwbRiFGz LILsACKHBlTmlzVB8s6qTtDrtjP8JHV7NSbxa84QSL0XcHUhMMVSGNvi5gr5dSom TJsrfxJTX1+uLRDvxhqTdGoPIl9/J9ekTMQ/C8WNlSwAJbQDsAFG4PZhu3M6haH3 cH7l0VCDbewq42axDNlwbY7/jw9bvHrkU+PjSmRBOnyUuGJocHGDywqYA6vQLoO3 /UcSWFQ+/QFHxGVC4f6SrM2c+GCPBTLPUVUAJEwi0U6OnB7d6ObLsMEy0vY56GPJ TGSJtUhxMFp/zDhMdBeviRDX1tLpT5rjP34F1Iee3KMZh7tEY45NlcIhdCJ8bAbt j+X7YVI15jrpX3XVBNilTgvAZy0/c4ZBjRcr44GwbHQvYE8N/9WEIf6d8I+9/d6S mRZJVNEcHEA+bpWGT/S7nzgxrtusFejR5JxxPIes8AVsFKNkCq0BkxiwRWIrIEEJ 7TzdVZdZFc8OWthKlkVuylrbg2BJfARx7CMnywuj/gx23lf9B9IkgDRptPwJQCJm +AtNujk0BUTjh7VazWK+FBUqHDciZQEGDEksy24zMW+BxZwLUvQXpXUnKT9tzMLm YI7x0MeDKSd+lhi36Z5EJHyPGB0KPZ8KNRV49WIwCgYIKoZIzj0EAwMDZwAwZAIw a/88UepYCuhkuUtwwL4WGsvEO4fN+07Togp9ksspYhpDvgEZSrn/oEwx7cNRbl7e AjBk/QR+Nif+U8CfM9bqs9SEww+KEykj+uyAud/C/qt97HsI7j7I2ECuy3SJL6vW l7I= -----END CERTIFICATE----- ================================================ FILE: test/assets/bad.txt.sig ================================================ MGUCMQDVGmInKk9wYEfCmnp+kPnLYM/P5B9FXR8Ec7AoLRrq+qExIWS9gcg0GPPYbFkqX7gCMAsGbuVHKJedWNF6vnV4J+3p8u8MhKvBTP+gBVeSZU1CuvULwDfU15EDEwgitIBgiA== ================================================ FILE: test/assets/bundle.txt ================================================ DO NOT MODIFY ME! this is "bundle.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle.txt.crt ================================================ -----BEGIN CERTIFICATE----- MIIC5zCCAmygAwIBAgIUJ3vpewdf6e91rgjqCqagstF4qn8wCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjMwNDI2MDAyMTA4WhcNMjMwNDI2MDAzMTA4WjAAMHYwEAYH KoZIzj0CAQYFK4EEACIDYgAE2sd6+lOBcn5MXtnbwca7zcwpprl7GUZiKTO9IWpA UfVTtx+BXGHQCRwsFy/d7dLlf4hurIqhzMD5yaC2kcU9/8c9G55JyBXF8Dx5SQm9 y2rPWFIdm29Ql9A3I3yyEFyPo4IBbjCCAWowDgYDVR0PAQH/BAQDAgeAMBMGA1Ud JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTlaUfjpiXGhBP3hOCW0JJZDSPxgzAf BgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAYBgNVHREBAf8EDjAMgQph QHRueS50b3duMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dp bi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dp bi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5 MZYC8pwzy15DQP6yrIZ6AAABh7rveBsAAAQDAEcwRQIhAKOZPMN9Q9qO1HXigHBP t+Ic16yy2Zgv2KQ23i5WLj16AiAzrFpuayGXdoK+hYePl9dEeXjG/vB2jK/E3sEs IrXtETAKBggqhkjOPQQDAwNpADBmAjEAgmhg80mI/Scr0isBnD5FYXZ8WxA8tnBB Pmdf4aNGForGazGXaFQVPXgBVPv+YGI/AjEA0QzPC5dHD/WWXW2GbEC4dpwFk8OG RkiExMOy/+CqabbVg+/lx1N9VGBTlUTft45d -----END CERTIFICATE----- ================================================ FILE: test/assets/bundle.txt.sig ================================================ MGUCMQCOOJqTY6XWgB64izK2WVP07b0SG9M5WPCwKhfTPwMvtsgUi8KeRGwQkvvLYbKHdqUCMEbOXFG0NMqEQxWVb6rmGnexdADuGf6Jl8qAC8tn67p3QfVoXzMvFA61PzxwVwvb8g== ================================================ FILE: test/assets/bundle.txt.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": { "x509CertificateChain": { "certificates": [ { "rawBytes": "MIIC5zCCAmygAwIBAgIUJ3vpewdf6e91rgjqCqagstF4qn8wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwNDI2MDAyMTA4WhcNMjMwNDI2MDAzMTA4WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2sd6+lOBcn5MXtnbwca7zcwpprl7GUZiKTO9IWpAUfVTtx+BXGHQCRwsFy/d7dLlf4hurIqhzMD5yaC2kcU9/8c9G55JyBXF8Dx5SQm9y2rPWFIdm29Ql9A3I3yyEFyPo4IBbjCCAWowDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTlaUfjpiXGhBP3hOCW0JJZDSPxgzAfBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAYBgNVHREBAf8EDjAMgQphQHRueS50b3duMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABh7rveBsAAAQDAEcwRQIhAKOZPMN9Q9qO1HXigHBPt+Ic16yy2Zgv2KQ23i5WLj16AiAzrFpuayGXdoK+hYePl9dEeXjG/vB2jK/E3sEsIrXtETAKBggqhkjOPQQDAwNpADBmAjEAgmhg80mI/Scr0isBnD5FYXZ8WxA8tnBBPmdf4aNGForGazGXaFQVPXgBVPv+YGI/AjEA0QzPC5dHD/WWXW2GbEC4dpwFk8OGRkiExMOy/+CqabbVg+/lx1N9VGBTlUTft45d" } ] }, "tlogEntries": [ { "logIndex": "7390977", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1682468469", "inclusionPromise": { "signedEntryTimestamp": "MEUCICSJs5PgN4W3Lku3ybrwfNLAKMWaOvffg2tnqm19VrWEAiEA16MVPsWDoaAljsxGefpQazpvYfs1pv8lzdgZQ0I4rH0=" }, "inclusionProof": { "logIndex": "7376158", "rootHash": "LE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=", "treeSize": "7376159", "hashes": [ "zgesNHwk09VvW4IDaPrJMtX59glNyyLPzeJO1Gw1hCI=", "lJiFr9ZP5FO8BjqLAUQ16A/0/LoOOQ0gfeNhdxaxO2w=", "sMImd51DBHQnH1tz4sGk8gXB+FjWyusVXbP0GmpFnB4=", "cDU1nEpl0WCRlxLi/gNVzykDzobU4qG/7BQZxn0qDgU=", "4CRqWzG3qpxKvlHuZg5O6QjQiwOzerbjwsAh30EVlA8=", "Ru0p3GE/zB2zub2/xR5rY/aM4J+5VJmiIuIl2enF/ws=", "2W+NG5yGR68lrLGcw4gn9CSCfeQF98d3LMfdo8tPyok=", "bEs1eYxy9R6hR2veGEwYW4PEdrZKrdqZ7uDlmmNtlas=", "sgQMnwcK7VxxAi+fygxq8iJ+zWqShjXm07/AWobWcXU=", "y4BESazXFcefRzxpN1PfJHoqRaKnPJPM5H/jotx0QY8=", "xiNEdLOpmGQERCR+DCEFVRK+Ns6G0BLV9M6sQQkRhik=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8050909264565447525\n7376159\nLE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=\nTimestamp: 1682468469199678948\n\n\u2014 rekor.sigstage.dev 0y8wozBEAiBbAodz3dBqJjGMhnZEkbaTDVxc8+tBEPKbaWUZoqxFvwIgGtYzFgFaM3UXBRHmzgmcrCxA145dpQ2YD0yFqiPHO7U=\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HVUNNUUNPT0pxVFk2WFdnQjY0aXpLMldWUDA3YjBTRzlNNVdQQ3dLaGZUUHdNdnRzZ1VpOEtlUkd3UWt2dkxZYktIZHFVQ01FYk9YRkcwTk1xRVF4V1ZiNnJtR25leGRBRHVHZjZKbDhxQUM4dG42N3AzUWZWb1h6TXZGQTYxUHp4d1Z3dmI4Zz09IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNMWVrTkRRVzE1WjBGM1NVSkJaMGxWU2pOMmNHVjNaR1kyWlRreGNtZHFjVU54WVdkemRFWTBjVzQ0ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwNUVTVEpOUkVGNVRWUkJORmRvWTA1TmFrMTNUa1JKTWsxRVFYcE5WRUUwVjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlRKelpEWXJiRTlDWTI0MVRWaDBibUozWTJFM2VtTjNjSEJ5YkRkSFZWcHBTMVJQT1VsWGNFRUtWV1pXVkhSNEswSllSMGhSUTFKM2MwWjVMMlEzWkV4c1pqUm9kWEpKY1doNlRVUTFlV0ZETW10alZUa3ZPR001UnpVMVNubENXRVk0UkhnMVUxRnRPUXA1TW5KUVYwWkpaRzB5T1ZGc09VRXpTVE41ZVVWR2VWQnZORWxDWW1wRFEwRlhiM2RFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pVYkdGVlptcHdhVmhIYUVKUU0yaFBRMWN3U2twYVJGTlFlR2Q2UVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpTZUdocVEyMUdTSGhwWWk5dU16RjJVVVpIYmpsbUx5dDBkbkpFUVZsQ1owNVdTRkpGUWtGbU9FVkVha0ZOWjFGd2FBcFJTRkoxWlZNMU1HSXpaSFZOUTNkSFEybHpSMEZSVVVKbk56aDNRVkZGUlVodGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1hOaU1tUndDbUpwT1haWldGWXdZVVJCZFVKbmIzSkNaMFZGUVZsUEwwMUJSVWxDUTBGTlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEFLWW1rNWRsbFlWakJoUkVOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRkRjM2QyVG5odmFVMXVhVFJrWjIxTFZqVXdTREJuTlFwTldsbERPSEIzZW5reE5VUlJVRFo1Y2tsYU5rRkJRVUpvTjNKMlpVSnpRVUZCVVVSQlJXTjNVbEZKYUVGTFQxcFFUVTQ1VVRseFR6RklXR2xuU0VKUUNuUXJTV014Tm5sNU1scG5kakpMVVRJemFUVlhUR294TmtGcFFYcHlSbkIxWVhsSFdHUnZTeXRvV1dWUWJEbGtSV1ZZYWtjdmRrSXlha3N2UlROelJYTUtTWEpZZEVWVVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1d1FVUkNiVUZxUlVGbmJXaG5PREJ0U1M5VFkzSXdhWE5DYmtRMVJsbFlXamhYZUVFNGRHNUNRZ3BRYldSbU5HRk9SMFp2Y2tkaGVrZFlZVVpSVmxCWVowSldVSFlyV1VkSkwwRnFSVUV3VVhwUVF6VmtTRVF2VjFkWVZ6SkhZa1ZETkdSd2QwWnJPRTlIQ2xKcmFVVjRUVTk1THl0RGNXRmlZbFpuS3k5c2VERk9PVlpIUWxSc1ZWUm1kRFExWkFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=" }, "signature": "MGUCMQCOOJqTY6XWgB64izK2WVP07b0SG9M5WPCwKhfTPwMvtsgUi8KeRGwQkvvLYbKHdqUCMEbOXFG0NMqEQxWVb6rmGnexdADuGf6Jl8qAC8tn67p3QfVoXzMvFA61PzxwVwvb8g==" } } ================================================ FILE: test/assets/bundle_cve_2022_36056.txt ================================================ DO NOT MODIFY ME! this is "bundle_cve_2022_36056.txt", a sample input for sigstore-python's unit tests. this has a corresponding bundle that is valid, *except* that the included log entry is from a *valid but unrelated* bundle (specifically, for an identical input signed immediately after this one). DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_cve_2022_36056.txt.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { "rawBytes": "MIIC1TCCAlugAwIBAgIUT8ug/4mjvLaDqXd4GKS6wmjq6MAwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDA1MjIwODEzWhcNMjQwNDA1MjIxODEzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECiYUx1SVwX5EHulBZv0FOEJ9AYXmCMOS8QVJnU1jY6xY6t4DCfaGwRU2iRIx8l4MmRKw8dwK8iA4/28TZt1HFKOCAXowggF2MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUvL83tyuyhCcA6zBgQlsrD9b2z5owHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjrBOHmkAAAQDAEgwRgIhAN/KC24XuwGgJRGpkvtzVVJSgEneKCV6PyM41Rul8gV0AiEA32ZU52ea/lCdPEzWTZxkdVbciAcsrATA+3D/o925g8owCgYIKoZIzj0EAwMDaAAwZQIxAO5FDiCQ79R69r6gyTgWhqADiisSZ7udiZGwRUWZcrBAYMKTw5Hy+1R/uKZcZ6jZKAIwFADtSVbmaXwC99hp++4aVyGo781VSiR5hIVRbFM+5l+psqG45/06bQy+Yj4EtrsY" }, "tlogEntries": [ { "logIndex": "26084047", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1712354907", "inclusionPromise": { "signedEntryTimestamp": "MEUCIDMn04X1vVbjq+WBhC0jv9M3Py5KLujlCp9zaA5eUdNAAiEA3lalF4OHKNLmlKq06z2Zg9jtQYA7NxJ16zV6MglvHZI=" }, "inclusionProof": { "logIndex": "26069228", "rootHash": "Wr9rTCceIRRp9phvQmZTrPlNXo5b7i+9pIRkRSA9fG8=", "treeSize": "26069230", "hashes": [ "flCB8VB67ZGa6K2ZEtDTtgtm96F3EjjtFvnGXwPOYT8=", "OzTdU4mq5jqXJ11gLmeEuCaLkxubkd4WVVwWUmZzgko=", "JV1urrvYBsls45EY/TJOuoRH3ho9y0nY1RvEgj1LWAs=", "VVpzU8MjvLgCT86Q0pSh57MzNiLGOphMU8kg9KAS9Lk=", "Nre+FErsP3TpqQY1RK7/b0WAL8fQx1bSbAuKjFSYvWg=", "jp/0CawpaDTbd+wM+aqjsO+AOVmIGunMId2ODziREU8=", "hSeZIoNlyUSqlJ6UyVfZIv17plm/YOvzrYEukkUh3OM=", "QdTMKazLZtCbvsCOn7U68L/vwKCJtgYyzRdxzbP3wcA=", "1P/q3R3vArPmJE+OmmcIRlBnXa/F2drYwklLngyaNXU=", "QyPS/J6veDqojEZv/v/8V1SpurFS22qOdFsQw1ZZH24=", "zL40ndFRmx2oQWFRdGwPjCl5BubNud42vN+OfvM9z9g=", "arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=", "Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=", "rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8050909264565447525\n26069230\nWr9rTCceIRRp9phvQmZTrPlNXo5b7i+9pIRkRSA9fG8=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiEAybn4EqPmEte82KeRUVEj5Kihrrm/72Bei84AF7CrPSwCIDANN3hLoyAiE5gN/3R2O4GRO+CvHZpsP2ZMB84X1Pa2\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjODdiNWIyMTNhYjA4YjMyOTcwYzEwNmQwYzdlNDQyM2U2N2Y1NDQ5YzJmZDJkMWU5YjRlYTZjYzQzYjZlZTdjIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURacnZDMjVxNGRlbStIb0liZ3BLNHI2MHZyUjhyay9CaEgvbVp3enROYXBnSWhBTm5KK1JFNXFiZ0xFR2lOeXN1OFVvS0lFL1diSWJaT1ZCL3hCVXZMbFRPZCIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhha05EUVd4MVowRjNTVUpCWjBsVlF6Rk9hbmRVZUU5eU1FZHhWMGNyZVZoUWNuWlZLMDVZUldWbmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVRGTmFrbDNUMFJKTTFkb1kwNU5hbEYzVGtSQk1VMXFTWGhQUkVrelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ0ZUVwQlVWQm5WWFpLTUZwWVJrdEVOVkV2WjJKSE0zRnZiMFFyYUdaVlRURTFWM1FLUm1aT1NUZHphemc0TVVNNFRUWXhSMDE2WWl0R2JsbEVhVkpUT0hCb1ZuQllWbEpyYUVOd1lWcEZZVloxSzJVeFVUWlBRMEZZYjNkblowWXlUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZyT0N0aUNtWXdMMFUzUkVwTGRVVlpNR3g0VjJwM1NUSlVZVUZyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwZDFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpsQ1NITkJaVkZDTTBGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFja0pQVmxCalFVRkJVVVJCUldkM1VtZEphRUZOYlZWSVVFOTRiWEE0VFdkblRtWndjbXRsUkdGbFdFbFZabnA1YlROVVEwdDVhV2xQUzNkd2VYQkdDa0ZwUlVFemRIWlVTbEJxV0dSMk1VMWtPSGd4WlRSWUt6RjFabWxWYUU5R1ZXdHFTVmxIVjA1NFpUQmFVeTlSZDBObldVbExiMXBKZW1vd1JVRjNUVVFLWVZGQmQxcG5TWGhCVFhNNVJGWmpNSHBEVjNSVmFFcEhPSEprVjJORGRHaHlNVmRZWkVGMllrODFSMFo2VEZsTVoyVlVhSFZJYzBZdk0yMXRkRmRpUWdwd1pVUkhZamtyZWpCblNYaEJVRmRRWVhFM1FqWllSamh5Y1ZwVFQzVTJVRWRwVFhOT2NXTTFRMmRPVUNzNFNWbFRVemt4V0U5aE1FRlJWamRoTUdzd0NtVkdVelZGZWtwNFRVMVVRMlJuUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "yHtbITqwizKXDBBtDH5EI+Z/VEnC/S0em06mzEO27nw=" }, "signature": "MEUCIQCr7CB5uKBUYJQxVCiHA1kCZHusFBjKfI1G9cVcPfPDmAIgSzjMGvzMAI3/OvnDoVGWi2kVwXfuyCSqH/2EUjXA93o=" } } ================================================ FILE: test/assets/bundle_invalid_version.txt ================================================ DO NOT MODIFY ME! this is "bundle_invalid_versions.txt", a sample input for sigstore-python's unit tests. this has a corresponding bundle that is valid, *except* that the bundle's media type is nonsense. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_invalid_version.txt.sigstore ================================================ { "mediaType": "this is completely wrong", "verificationMaterial": { "certificate": { "rawBytes": "MIIC1DCCAlmgAwIBAgIUfpvqrH5roQBYuCS0/ENuM/EIxNEwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwMzE1MjA0ODIyWhcNMjQwMzE1MjA1ODIyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExXe2NZ78guW75Lg7EalUisXCGK6+RhEMWiZtZnMliQ57FeS7VOhX+7yZ496R0e5K4y57q3xtA9rDpU2xG1hOC6OCAXgwggF0MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU3DU96/CFVHuUeFfcBFg54Gubt5IwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjkPfdqwAAAQDAEYwRAIgXu5ZYuDEPccUzU7hxM+c80mCgeSoBd3a3HXQTBY5RkACIHOcd+aHPP9q6WVBxZ1uz66LYUgttOv5vNLY4HoJHO8eMAoGCCqGSM49BAMDA2kAMGYCMQD1aJ1vd2ap0HY3ULvRnTpcZdYy5g5Hr4G3cGqSsjN+hVVqlUdvdmxNMnViF6riRBoCMQDuughnn3g3i6PL+rXhLzYRLnYndBbGpZtJNeOFBoKw5CJQ56v6Kep4QNFK/OHVS8w=" }, "tlogEntries": [ { "logIndex": "24949628", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1710535702", "inclusionPromise": { "signedEntryTimestamp": "MEUCIQC0UGv+Vp65x5KxazYIsqUWtXRHt1YS3yLc6The2GB1vAIgY/OaE2hLlr8s1Q4Tcc3kQfaUFxn/ze5zSpOiONFtyDE=" }, "inclusionProof": { "logIndex": "24934809", "rootHash": "PDv3TU4otWhy8KPXPPenHw2Ewvhv39gLYJyb7y4T7Vg=", "treeSize": "24934810", "hashes": [ "75BvLzwZrn9W1mMwiLlys6m8bdE6BzBtLoWb1TzD2IE=", "jeHvmA4WMzBKO4NTgU92d4SergID/98qHjQa7lyHM3I=", "PJ5j70iRcvuRdyqxfxTzriwR3DYCKxgn55n/x8bc88Q=", "fX6uhDvkI9uobrWeHxotNKFCnnQS6o+4DIx5VJEXPGI=", "Yi12x3CrLnY6ZtXzOD6Bko+pi9TgppbBd9Q/53BVAW4=", "CqWx5l0KONghRL4Y/KQCrseE3N5tobvzrSlwx6D72/Q=", "C4mAo6ST72aStQFQAjWnmHqGnNwkAbH/T1ZSGGW+oZQ=", "FMerZZ8JrR1y7HaRAWWMTHMF7Ogi+5ByNHzxnvd7wL4=", "Vq6uGE0RoCr4qvHzVA05MwXkyWEEB4quqDcBI4Xp7Yk=", "YEBzcKtzqBbRZdcxjYtCB3drVWOmpALLwzh9v3oDdGA=", "QTIhNTVhTR/6O+88G85QIfQsMUF4gIaq2ekPotTnXZc=", "hURLr2hArDJRWqYQcMBNoXVK/G0/rtoljhC5trcmdZ0=", "xj8ziVN4lEi2nf7WysmgKHG6LrsGd6QTwVBAL/yKdr0=", "SPEhngb75Zn+e/TNUQqKUKvY3Q7GE+M4BuniyqnCKLs=", "rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8050909264565447525\n24934810\nPDv3TU4otWhy8KPXPPenHw2Ewvhv39gLYJyb7y4T7Vg=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiAmS+VF6VZ1ylUPQ3v/bUIJwYKfNrdocQ1VTJz6lF6TOwIhAI7by9+dK+jqTsniPWlw1SL6gCGq4FLJ8Wz2evZZ8NPq\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3YTFmMmIyM2VjOWFkZDE2M2M2ZDBiYmI0NTRhZjQ1ZTE0ODNiOWJlYzdiNzJhZWExYjRmZGYwZTJmNTY0NGYwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQ2pZbnpKMXFiOE9IL3M4U3czUXg1VEJPMWM5bDFQK3NDTXBwcEJqN3VPd0FpQmY2cTE1R2VsN3JXdGUzMnR1VmtLVVpCZXJtVDZYTjRBL3NxL2tkWmhkdXc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4dFowRjNTVUpCWjBsVlpuQjJjWEpJTlhKdlVVSlpkVU5UTUM5RlRuVk5MMFZKZUU1RmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UlRGTmFrRXdUMFJKZVZkb1kwNU5hbEYzVFhwRk1VMXFRVEZQUkVsNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVY0V0dVeVRsbzNPR2QxVnpjMVRHYzNSV0ZzVldseldFTkhTellyVW1oRlRWZHBXblFLV201TmJHbFJOVGRHWlZNM1ZrOW9XQ3MzZVZvME9UWlNNR1UxU3pSNU5UZHhNM2gwUVRseVJIQlZNbmhITVdoUFF6WlBRMEZZWjNkblowWXdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlV6UkZVNUNqWXZRMFpXU0hWVlpVWm1ZMEpHWnpVMFIzVmlkRFZKZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwVVZsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpkQ1NHdEJaSGRDTVVGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFhMUJtWkhGM1FVRkJVVVJCUlZsM1VrRkpaMWgxTlZwWmRVUkZVR05qVlhwVk4yaDRUU3RqT0RCdFEyZGxVMjlDWkROaE0waFlVVlJDV1RWU2EwRkRDa2xJVDJOa0syRklVRkE1Y1RaWFZrSjRXakYxZWpZMlRGbFZaM1IwVDNZMWRrNU1XVFJJYjBwSVR6aGxUVUZ2UjBORGNVZFRUVFE1UWtGTlJFRXlhMEVLVFVkWlEwMVJSREZoU2pGMlpESmhjREJJV1ROVlRIWlNibFJ3WTFwa1dYazFaelZJY2pSSE0yTkhjVk56YWs0cmFGWldjV3hWWkhaa2JYaE9UVzVXYVFwR05uSnBVa0p2UTAxUlJIVjFaMmh1YmpObk0yazJVRXdyY2xob1RIcFpVa3h1V1c1a1FtSkhjRnAwU2s1bFQwWkNiMHQzTlVOS1VUVTJkalpMWlhBMENsRk9Sa3N2VDBoV1V6aDNQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "eh8rI+ya3RY8bQu7RUr0XhSDub7HtyrqG0/fDi9WRPA=" }, "signature": "MEQCICjYnzJ1qb8OH/s8Sw3Qx5TBO1c9l1P+sCMpppBj7uOwAiBf6q15Gel7rWte32tuVkKUZBermT6XN4A/sq/kdZhduw==" } } ================================================ FILE: test/assets/bundle_no_cert_v1.txt ================================================ DO NOT MODIFY ME! this is "bundle_no_cert.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_no_cert_v1.txt.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": { "x509CertificateChain": { "certificates": [] }, "tlogEntries": [ { "logIndex": "12299864", "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1675126613", "inclusionPromise": { "signedEntryTimestamp": "MEQCIHznNDQGR9OoggiqwdIy1XL+s0DIN8CKhy7HeeoL1TBLAiAbPK3/+x/j693cYidV0kKNf6qNQQcVQiYoDmc/GPSlNA==" }, "inclusionProof": { "logIndex": "8136433", "rootHash": "Q0UPuyoLnMKq1ovXXecjQ3T9S+Si3psOoufy+q8rXXo=", "treeSize": "8136434", "hashes": [ "cdd3+Ki2g1oCwg8BSGNbjGjj1vnWCoW/bLvg6BTVewc=", "WUmDxZ3E04pjC/Boy8pxfDs0Buj3VTncmMNKpjJqsZ8=", "cVwcUBx0BZZQR36WQaQu0YM7QD7wv2rAAGdv9mbsl6A=", "upKFhQ0+3Te5YxqUVtD8w1JsYwvexrTLLRVvkiEzk4Y=", "M4k48iae5vhJ5K85ZwV5YJHrJXYwEQQxJgxeiIBR6O4=", "BaYLbIqmLbsAG8A+hzSvk3Blffx41WgBvn1c+HtvaPk=", "8SbpbSXXlm8lFn5KsRE6H+U+ZUj7cZd/JsBckNDHrY4=", "Xhw0UBkdQpGoX9d4nPr3dfz19Qxe1qKvPdbsEnuGpzQ=", "XrQ+ynp2Pi6q+yvC/JY+eAIoPPGpB2Y4JCF3sWaZQsA=", "VSPNAZ/qk9AYNPxCn3CLcArrcsg1pzFhzbkAP49OgHI=", "S232ZNlVK8JNSKTH1WWagnAGXh/tvDkQOwEKsjWoeFA=", "YWQp22x9IMWw7/Gm5RLqV6BzS5SuC8fJFGeUY+Aaf7o=", "WkrRU0sedw8Cv94N4VAIppcc8f+/qWP8nCpkSXOo0tE=" ] }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIxYmMzN2Q0ZmVkM2ZkOTYwYmRmOWQwOTVmODcyODNiYThhZDFhYjQ0ODFmMzhhMzRlODAwNGYwYzU2ZmZlNzhkIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HVUNNR25uRUxCOXkzcStlUzdHSHJnYTJXZDBodWpXUTNMSjRXeVhoL1VMVWl3dXZmR0hOSzh6WFFuUUFOWmxSdEg2a1FJeEFKR0EvaXQ3T1ZiNDRBb3A2VDhETzVzK1RhamNuN0VnRng0MkRaaVdYU3JGZDBWTUl6bjRVRm80U0UxQy9VdkhhZz09IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VONFJFTkRRV3QxWjBGM1NVSkJaMGxWU1dwV1JVRlBkazFGY1ZVMU4zaDNPR3A1ZUhRMFkwZGhVa2hKZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwMVVUWGhOUkVFeFRtcFJNMWRvWTA1TmFrMTNUVlJOZUUxRVJYZE9hbEV6VjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlV4dVFrazNiRlEwTUhSU1owbE9ObGt3TlVNd01YZFJSMUZuY0dsTGFWZFhNVk13WkZGQ1RXWUtVME5IU2pOWFdEQm9hVlZQU0RSMVJHMVhZbXhCZGpscmNrUlVRa2RwTURoaGJHZDBSRzB5Y25rcmNqWjJTa3d6ZVZOc1RWUnNTMkZIVHpCNWNFWkZRd295VUhGdVpFZHhjMlkxYmtKdGNrVkVUWE5ZWldoWFZpOXZORWxDVkZSRFEwRlZhM2RFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pUUkhSQllqWlJVMGRJUjJkd2JtbHZjREJRZDNKVlYyVlBkVE5FUVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpVWmpBcmJsQldhVkZTYkhadGJ6SlBhMjlXWVV4SFRHaG9hMUI2UVhGQ1owNVdTRkpGUWtGbU9FVkpSRUZsWjFKNGFBcGlSMVkwVEcxT2FHSlhWbmxpTWpWQlpFaEthR0ZYZUhaYWJVcHdaRWhOZFZreU9YUk5RMnRIUTJselIwRlJVVUpuTnpoM1FWRkZSVWN5YURCa1NFSjZDazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRk9NRGtLVFVkeVIzaDRSWGxaZUd0bFNFcHNiazUzUzJsVGJEWTBNMnA1ZEM4MFpVdGpiMEYyUzJVMlQwRkJRVUpvWjFaVWFtVm5RVUZCVVVSQlJXTjNVbEZKYUFwQlRVOWhkVlVyVXpGeGJWQkJNblpOUzJ4QldHUlpiM1p0ZDNwUk5EVTVhMlV6TmxsdE1ERnZhRXRhVkVGcFFrRmhTa0pHVVdKSVNtRTVlREpGY0VsdkNraEZRVEUzTjBsWlpYVjRWVEJ5UkdGdU1sUkdWblJsWW1ORVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1dVFVUkNhMEZxUW0xc1ZqWjVOVFZOTWtaMWR6QUtPVUl6SzNkdmRFTjRhVWRCT1V4TGJUZHlWVzVFUVdaWFdYTlVWek5HTjJWaE1XVkplVkJsYlZvMVlXVnVTV3BoU0RGRlEwMUhSMUF2TjJoa1FYRnRhd3BNUkhoRFRFSmthbHBtTUROV2FVTnllSEpTVEVkQldVUjNRa2w2VERaemFWQlRiV3AyZDBSUFJWSkhOV05sVFVGVlRDOTJkbEU5UFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "G8N9T+0/2WC9+dCV+HKDuorRq0SB84o06ABPDFb/540=" }, "signature": "MGUCMGnnELB9y3q+eS7GHrga2Wd0hujWQ3LJ4WyXh/ULUiwuvfGHNK8zXQnQANZlRtH6kQIxAJGA/it7OVb44Aop6T8DO5s+Tajcn7EgFx42DZiWXSrFd0VMIzn4UFo4SE1C/UvHag==" } } ================================================ FILE: test/assets/bundle_no_checkpoint.txt ================================================ DO NOT MODIFY ME! this is "bundle.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_no_checkpoint.txt.bundle ================================================ ================================================ FILE: test/assets/bundle_no_checkpoint.txt.crt ================================================ MGUCMArXoJGZeHwbgH1sCqhkv2f2J9XntOwIP1MrcXoqBsU3AAyeyB/1ggizV6ScbQFPtQIxAIoH4b4PCIbqufTc6UG4eTchZgYh5hW8m4BOkhbCEiCzKsaZ0Trg8+Hm1N8egtVgYw== ================================================ FILE: test/assets/bundle_no_checkpoint.txt.sigstore ================================================ {"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", "verificationMaterial": {"x509CertificateChain": {"certificates": [{"rawBytes": "MIICwjCCAkegAwIBAgIUNRulROGJTUrEWvs9h68bMocfMbcwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwMTMwMjI0MjA4WhcNMjMwMTMwMjI1MjA4WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC4pHa0GudExSiDdn1RwUrytQUraA6CkGiiuVWnP661vvPfETx/3xr5/Q/8sy00tg7LjR5yFggFKSmM8E7Q03YAWZvORioljrokKVSLbJ7tEVtiJsraGaQYfcLcfk+Ei+o4IBSTCCAUUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBSgjmExD0FvLB3+YdpMkbc8D/aTpjAfBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGGBNhGsgAABAMARzBFAiEAyCATYmUVra04RNbRWA1B9IvOQb1Oo6dWbVcmD7lpDA4CIHuU5JUEd6+mud17S2sA0I+lZdknTw3fxK3wwMhWo4BrMAoGCCqGSM49BAMDA2kAMGYCMQCvIjyVjvhvgoLWD9D2S/GKsvCXfAZXR4V+JJvBKrqNJBclJKrEWJoVEryC09nyi+cCMQDsg29gfCZGmtQo2I/1JV3eypmnnrqAX/ot3RE5O2iTVwpgVD+G+ZPBX0xb0nQBVqI="}]}, "tlogEntries": [{"logIndex": "2798447", "logId": {"keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1675118528", "inclusionPromise": {"signedEntryTimestamp": "MEUCIQCvADnaopfUq3ZHmMH5axUTAnVsFm+lRwZTzojS/S/j6gIgTBFqilaERr4ynGts13KQnhW+N+f3SZHuKEPa56TsGjk="}, "inclusionProof": {"logIndex": "2783628", "rootHash": "yI+q1pOVBmLshdZ/AMZyobBGoZSnlP7DEJKa1oih/EM=", "treeSize": "2783629", "hashes": ["M2NdF1n5XRkCCOSIfaQjxtlgrZAtEmt0gPiPc4RERIQ=", "xdOVB9j9HhIpNr3XuX1x3h3YeQbiG3C2ORYLa53P9xk=", "nijvvfATxTieswSd7U9UXoT4CGrSShbXN6vwgF0hz3o=", "i045tKzGMiRsPd+6s0019t2W/w/mPWYAMFQazJ9Z9SI=", "Te4YkwkpHbNU40NJrsh0R/dYUd7IzsjfgscYw6qulqs=", "jiYMh5IprbGRK0sVt0QT4jK3+/wJvwhwO9zm+oJ+vyI=", "oDOc4/cWh/p+nUSrwVD3sGbbXaOdfmqx8ed9TBf/6GE=", "Li4l4euEirqV/WiWSGmyrvIQoYF80WAFTcGY2SXG5tY=", "GkJkTsUxj1BshWxCshtF5bL+BVbG7ZPSzJe157aFBd4=", "P7oQEMYLmrkMhQLUuYWXJ2mL524qm2+ib1buwM/lvic=", "VwBj5hN1tw74kRJeHAQaqdSWrXWk7Zb4c1PJfrpiKNw="]}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HWUNNUUN3UCtVM25QcE9RaWpScDJPK2c2UDhSQzRnZlVMK1BDTkRIcGJmekhqbHVlVWdIanNOZE5SMng2dTRkL0ZpL1ZrQ01RRFExM24vS1hmbEhRekltbG9xRGxPdkxBT2JlR3BZUzdkWUIrWEpIdGw1dnNGUW51R0FHZ1Byei92NWxrQjY2ems9IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VOM2FrTkRRV3RsWjBGM1NVSkJaMGxWVGxKMWJGSlBSMHBVVlhKRlYzWnpPV2cyT0dKTmIyTm1UV0pqZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwMVVUWGROYWtrd1RXcEJORmRvWTA1TmFrMTNUVlJOZDAxcVNURk5ha0UwVjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlVNMGNFaGhNRWQxWkVWNFUybEVaRzR4VW5kVmNubDBVVlZ5WVVFMlEydEhhV2wxVmxkdVVEWUtOakYyZGxCbVJWUjRMek40Y2pVdlVTODRjM2t3TUhSbk4weHFValY1Um1kblJrdFRiVTA0UlRkUk1ETlpRVmRhZGs5U2FXOXNhbkp2YTB0V1UweGlTZ28zZEVWV2RHbEtjM0poUjJGUldXWmpUR05tYXl0RmFTdHZORWxDVTFSRFEwRlZWWGRFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pUWjJwdFJYaEVNRVoyVEVJeksxbGtjRTFyWW1NNFJDOWhWSEJxUVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpTZUdocVEyMUdTSGhwWWk5dU16RjJVVVpIYmpsbUx5dDBkbkpFUVdwQ1owNVdTRkpGUWtGbU9FVkhWRUZZWjFKV013cGhWM2h6WVZkR2RGRkliSFpqTTA1b1kyMXNhR0pwTlhWYVdGRjNURUZaUzB0M1dVSkNRVWRFZG5wQlFrRlJVV1ZoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZDbVJYU1hWWk1qbDBUREo0ZGxveWJIVk1NamxvWkZoU2IwMUpSMHRDWjI5eVFtZEZSVUZrV2pWQloxRkRRa2gzUldWblFqUkJTRmxCUzNwRE9ETkhhVWtLZVdWTWFESkRXWEJZYmxGbVUwUnJlR3huVEhsdVJGQk1XR3RPUVM5eVMzTm9ibTlCUVVGSFIwSk9hRWR6WjBGQlFrRk5RVko2UWtaQmFVVkJlVU5CVkFwWmJWVldjbUV3TkZKT1lsSlhRVEZDT1VsMlQxRmlNVTl2Tm1SWFlsWmpiVVEzYkhCRVFUUkRTVWgxVlRWS1ZVVmtOaXR0ZFdReE4xTXljMEV3U1N0c0NscGthMjVVZHpObWVFc3pkM2ROYUZkdk5FSnlUVUZ2UjBORGNVZFRUVFE1UWtGTlJFRXlhMEZOUjFsRFRWRkRka2xxZVZacWRtaDJaMjlNVjBRNVJESUtVeTlIUzNOMlExaG1RVnBZVWpSV0swcEtka0pMY25GT1NrSmpiRXBMY2tWWFNtOVdSWEo1UXpBNWJubHBLMk5EVFZGRWMyY3lPV2RtUTFwSGJYUlJid295U1M4eFNsWXpaWGx3Ylc1dWNuRkJXQzl2ZEROU1JUVlBNbWxVVm5kd1oxWkVLMGNyV2xCQ1dEQjRZakJ1VVVKV2NVazlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ=="}]}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4="}, "signature": "MGYCMQCwP+U3nPpOQijRp2O+g6P8RC4gfUL+PCNDHpbfzHjlueUgHjsNdNR2x6u4d/Fi/VkCMQDQ13n/KXflHQzImloqDlOvLAObeGpYS7dYB+XJHtl5vsFQnuGAGgPrz/v5lkB66zk="}} ================================================ FILE: test/assets/bundle_no_log_entry.txt ================================================ DO NOT MODIFY ME! this is "bundle_no_log_entry.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_no_log_entry.txt.sigstore ================================================ {"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": {"x509CertificateChain": {"certificates": [{"rawBytes": "MIICxDCCAkqgAwIBAgIUERCmd8PPVzGcAn7smRhiMQ1rjgAwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwMTMxMDA1NzM1WhcNMjMwMTMxMDEwNzM1WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAErDdWhJLHi4E8pL5I6PnYm3O50xBNghdCsXj/zPkrKCRmkyax+WoZq+UdbuuNgER4rIRimdWvFGP/CpQWA8jcYFXeFTWbDDhBxYFPs9KWjq/a6BF7iYaHwFQl+o0Oo9IOo4IBTDCCAUgwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBScV4LgdhmkHru8fcV23gZwzC+JjjAfBgNVHSMEGDAWgBTf0+nPViQRlvmo2OkoVaLGLhhkPzAqBgNVHREBAf8EIDAegRxhbGV4LmNhbWVyb25AdHJhaWxvZmJpdHMuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABhgVUSb8AAAQDAEYwRAIgUagdrzc6Zr0XHzBfkPPeB+kSln9BChTOS3XLlwy1SGQCICyjI9i0PujwHtSC5AsFcrTGiBc0KeopXmYqXRN7A2vfMAoGCCqGSM49BAMDA2gAMGUCMQCagTgf3TMQpXMSMc3jREF+E8j1XngvBfgBNzcd1bbBfSUl2fyv7HMETgBzLTht/bQCMGeSULPeevErK9Jb7jRGbMBmSTNXLIPebx1zAijj8tTBW91z7v9Bjb/AmytwpALVSA=="}]}, "tlogEntries": []}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "m0afyXYei5Mwc7YnJfvJi1ScMxQZne1xzIsobvLG+5s="}, "signature": "MGUCMHKDIXsY9G/pbwFY23mDx1aXSZVpasnQKES5pFWz1NxayS0+dt2edIdDPaLrSuBGuAIxAP9MADAHRBp2dBqpvo8O8u7VLOMU8JMt1eSMjwMzJPRNuGkO8dgpX+Yyy1452fitKA=="}} ================================================ FILE: test/assets/bundle_v3.txt ================================================ DO NOT MODIFY ME! this is the input for bundle_v3, which tests support for "v3" bundles. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_v3.txt.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { "rawBytes": "MIIC1DCCAlqgAwIBAgIUO3tlVbLtvLPp+6zGOtep1SPkRigwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDAyMTkxOTA5WhcNMjQwNDAyMTkyOTA5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENdrfpgNU1Rjmz+j65rpJWKc08ruKYy4FX7nmmOnbauFZimsQXrdyDSXKNRtEXX4X3t/Amt+euwPDBh+eq7BCnqOCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUGRlBhD0wvzAfLb2dMWOgPrrJuRkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjqBAQZ4AAAQDAEcwRQIgeWUmtnD0MFUl5kkX7nbMdLWCsDGIPzdIlN+WaZF0TmkCIQC7+31saqrFe9RmduVZ2dxXhUPrajltuSDHb1vSGOcuHjAKBggqhkjOPQQDAwNoADBlAjEAn2+uuLHsnH9Db7zkIdF65YhiXbgMMF//iHc+B/QETK0HYVcOPTK3p46FUzXFD6xrAjAO2hrkfjBKANKjJJxHV3FVrtS+TR0GCP0HzC3D7Br95TXzfO7+j4Dd8/N/aAr6Ibs=" }, "tlogEntries": [ { "logIndex": "25915956", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1712085549", "inclusionPromise": { "signedEntryTimestamp": "MEYCIQD2KXW1NppUhkPPzGR8NrUIyN+MzZSSqGZQO7CzvhSnYgIhAO9AHzjbsr1AHXRHmEpdPZcoFHEwwMTgfqwjoOXVMmqN" }, "inclusionProof": { "logIndex": "25901137", "rootHash": "iGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=", "treeSize": "25901138", "hashes": [ "UHUr+lvxENI+G902oEsFW5ovQILgqO9mUWWxvvwHZZc=", "IcMBsbH3GRW8FX2CiL/ljMb45vzmENmhp5Yp/7IW998=", "SxC6nr0zP+a6kWb6nO2fmEtz8BYAbqEXc+dsqGLdRPM=", "sppZRSz/vdeLlavgvICrXHLeReMTJw98bs9HJ0I8WnE=", "c8lCSuBS6MzrRnt6OiyYjqhTyxUI/22gpVB7dblfDis=", "eJk64J6cMpIljPSX/72kH0kiIeElyypQm5vJ2gMMyHw=", "hbIK+jmAwQjU7Yi3iKvnfR1u7GNippk7QsRwJXIuRaw=", "tpHWIEB2vNU5ZmC68dj1Hh9cwQK083ozogA6zJ3cJ8A=", "arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=", "Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=", "rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8050909264565447525\n25901138\niGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiAMJJLbnNOnmizMbVBz9/A/qnMK15BudWoZkuE+obD6CAIhAJf6A3h2iOpuhz/duEhG3fbAQG9PXln4wXPHFBT5wT1a\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ZTZhZTlkZTU4YzExNzdiZWE2MTViNGZjYmZiMmZkNjg4ZThjNGI1MWMyZTU2YjZhMzhlODE3ODMzZWMyNGEyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRFFTSmk5YWVydFFobVQrY2UxaktOZENlNEtTY3NLR3E5ZlBtMzQyMkRCU0FpRUFoajFzeFo5Nm9ySVRzUXh5TUxJRFJKaW1wb3kxSjFNeWZsY1FWd2tremhzPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4eFowRjNTVUpCWjBsVlR6TjBiRlppVEhSMlRGQndLelo2UjA5MFpYQXhVMUJyVW1sbmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVhsTlZHdDRUMVJCTlZkb1kwNU5hbEYzVGtSQmVVMVVhM2xQVkVFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZPWkhKbWNHZE9WVEZTYW0xNksybzJOWEp3U2xkTFl6QTRjblZMV1hrMFJsZzNibTBLYlU5dVltRjFSbHBwYlhOUldISmtlVVJUV0V0T1VuUkZXRmcwV0ROMEwwRnRkQ3RsZFhkUVJFSm9LMlZ4TjBKRGJuRlBRMEZZYTNkblowWXhUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIVW14Q0NtaEVNSGQyZWtGbVRHSXlaRTFYVDJkUWNuSktkVkpyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwWjFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpoQ1NHOUJaVUZDTWtGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFjVUpCVVZvMFFVRkJVVVJCUldOM1VsRkpaMlZYVlcxMGJrUXdUVVpWYkRWcmExZzNibUpOWkV4WFEzTkVSMGxRZW1SSmJFNHJWMkZhUmpCVWJXdERDa2xSUXpjck16RnpZWEZ5Um1VNVVtMWtkVlphTW1SNFdHaFZVSEpoYW14MGRWTkVTR0l4ZGxOSFQyTjFTR3BCUzBKblozRm9hMnBQVUZGUlJFRjNUbThLUVVSQ2JFRnFSVUZ1TWl0MWRVeEljMjVJT1VSaU4zcHJTV1JHTmpWWmFHbFlZbWROVFVZdkwybElZeXRDTDFGRlZFc3dTRmxXWTA5UVZFc3pjRFEyUmdwVmVsaEdSRFo0Y2tGcVFVOHlhSEpyWm1wQ1MwRk9TMnBLU25oSVZqTkdWbkowVXl0VVVqQkhRMUF3U0hwRE0wUTNRbkk1TlZSWWVtWlBOeXRxTkVSa0NqZ3ZUaTloUVhJMlNXSnpQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "Xmrp3ljBF3vqYVtPy/sv1ojoxLUcLla2o46BeDPsJKI=" }, "signature": "MEUCIDQSJi9aertQhmT+ce1jKNdCe4KScsKGq9fPm3422DBSAiEAhj1sxZ96orITsQxyMLIDRJimpoy1J1MyflcQVwkkzhs=" } } ================================================ FILE: test/assets/bundle_v3_alt.txt ================================================ DO NOT MODIFY ME! this is the input for bundle_v3_alt, which tests support for "v3" bundles with the older ("alternate") v3 media type. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_v3_alt.txt.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.3", "verificationMaterial": { "certificate": { "rawBytes": "MIIC1TCCAlqgAwIBAgIUM8X9bqAVFpaofemSrcgku8oJ/T8wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDAyMTkyMDE2WhcNMjQwNDAyMTkzMDE2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4tx/Vclr8Yr3ArUW9kIEFuR4mynLKKScX2ECX+I4WsXi6Q/0JUoVM2B/U3e97BZcl/bWHEToeshhWiQLaGztX6OCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU7611Rsd/9kcw/cE/Fe3B5fUfpt0wHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjqBBR8IAAAQDAEcwRQIhAOb1ZBDYuTNMa1RVLWvER4K51+ugnGYPCZKCXwx3hb+DAiBXX1DDn6Xv9B/RC5s3ZMQfbZ6jN7DFXQqDi/r3GLMP2DAKBggqhkjOPQQDAwNpADBmAjEAvXkFJvo2uuPZ7L5aRixEkysAF24+vRmISzcE2qTrGbzmqCW1AFzbnFsLhllo8IEJAjEAgEr9lfJZJCDLE1kV9M3/nfsPD/6ZNtDbU0vRjeygnXgsXJkrf96SjZCCfIlzxczF" }, "tlogEntries": [ { "logIndex": "25915997", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1712085616", "inclusionPromise": { "signedEntryTimestamp": "MEUCIQCDbNwTMuX7lJt//HauYK0/RZ6UbKbYVR+vEr7rns4/ngIgSwRaRO2ody7uWMtIwe/ZRKwvl7+3Kn3IYKZDEj6CX8w=" }, "inclusionProof": { "logIndex": "25901178", "rootHash": "q0g3yMEVgKep9vgSfpTBZYld9mlsniTqXHzBAorxMtE=", "treeSize": "25901179", "hashes": [ "6HxJ5B0YCXus8f+tO/yVTLFaLZfwjiaOnBOmhSzIo8k=", "Oa+3NjADjkBP1F7UrrJ8l7melp/y6mIlgHuEEGdSDrI=", "B4/zyNNgeuMr+zPZ/+mSVl//HFmVSxVWsNL1dHh4hw0=", "NzOg27Ucfb8sHqU9tZnKC5VZFuIsRpDYoqmBAPzB42g=", "SxC6nr0zP+a6kWb6nO2fmEtz8BYAbqEXc+dsqGLdRPM=", "sppZRSz/vdeLlavgvICrXHLeReMTJw98bs9HJ0I8WnE=", "c8lCSuBS6MzrRnt6OiyYjqhTyxUI/22gpVB7dblfDis=", "eJk64J6cMpIljPSX/72kH0kiIeElyypQm5vJ2gMMyHw=", "hbIK+jmAwQjU7Yi3iKvnfR1u7GNippk7QsRwJXIuRaw=", "tpHWIEB2vNU5ZmC68dj1Hh9cwQK083ozogA6zJ3cJ8A=", "arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=", "Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=", "rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8050909264565447525\n25901179\nq0g3yMEVgKep9vgSfpTBZYld9mlsniTqXHzBAorxMtE=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiAt/kYsQHQLeEo7R5UmNw7n7Mhn07ihpmFDC0zF1OfHSAIhAPCVUCdlUxnW7tz9Ob3IsX7e3St7pMwz32414GQZ6woa\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0MTkxZTZiNGUyYjAyNjBlNmUyOTdkNDc5N2QyYTg3MzU4NDk2NWFmZWYwMjFiZjIyZjJiYjdiZWM0MTEwMmQwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUNVeFo1V1Z0SG9oUEVIVzZwZ0hUQTBvMFoyWGdtUklGOEUvKzBQVGE5YWVRSWdPblRMZHNpYnhXbFpkVlNtckJzNEN3R2JuRXloT0dKc0Z0KzQ2anpjQU1VPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhWRU5EUVd4eFowRjNTVUpCWjBsVlRUaFlPV0p4UVZaR2NHRnZabVZ0VTNKaloydDFPRzlLTDFRNGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVhsTlZHdDVUVVJGTWxkb1kwNU5hbEYzVGtSQmVVMVVhM3BOUkVVeVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUwZEhndlZtTnNjamhaY2pOQmNsVlhPV3RKUlVaMVVqUnRlVzVNUzB0VFkxZ3lSVU1LV0N0Sk5GZHpXR2syVVM4d1NsVnZWazB5UWk5Vk0yVTVOMEphWTJ3dllsZElSVlJ2WlhOb2FGZHBVVXhoUjNwMFdEWlBRMEZZYTNkblowWXhUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlUzTmpFeENsSnpaQzg1YTJOM0wyTkZMMFpsTTBJMVpsVm1jSFF3ZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwWjFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpoQ1NHOUJaVUZDTWtGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFjVUpDVWpoSlFVRkJVVVJCUldOM1VsRkphRUZQWWpGYVFrUlpkVlJPVFdFeFVsWk1WM1pGVWpSTE5URXJkV2R1UjFsUVExcExRMWgzZUROb1lpdEVDa0ZwUWxoWU1VUkVialpZZGpsQ0wxSkROWE16V2sxUlptSmFObXBPTjBSR1dGRnhSR2t2Y2pOSFRFMVFNa1JCUzBKblozRm9hMnBQVUZGUlJFRjNUbkFLUVVSQ2JVRnFSVUYyV0d0R1NuWnZNblYxVUZvM1REVmhVbWw0Uld0NWMwRkdNalFyZGxKdFNWTjZZMFV5Y1ZSeVIySjZiWEZEVnpGQlJucGlia1p6VEFwb2JHeHZPRWxGU2tGcVJVRm5SWEk1YkdaS1drcERSRXhGTVd0V09VMHpMMjVtYzFCRUx6WmFUblJFWWxVd2RsSnFaWGxuYmxobmMxaEthM0ptT1RaVENtcGFRME5tU1d4NmVHTjZSZ290TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "QZHmtOKwJg5uKX1Hl9Koc1hJZa/vAhvyLyu3vsQRAtA=" }, "signature": "MEUCIQCUxZ5WVtHohPEHW6pgHTA0o0Z2XgmRIF8E/+0PTa9aeQIgOnTLdsibxWlZdVSmrBs4CwGbnEyhOGJsFt+46jzcAMU=" } } ================================================ FILE: test/assets/bundle_v3_github.whl.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", "verificationMaterial": { "x509CertificateChain": { "certificates": [ { "rawBytes": "MIIGzzCCBlSgAwIBAgIUM29bvYkrDKnBVZmVeloTUMlZqNYwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwMzE5MjI0MTE1WhcNMjQwMzE5MjI1MTE1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1q8wmpmK0vesCD05ZE1o5Jyu+g/CtLZLXNEZiIomh1jquPMCZrhlPdOfzQws+E+IUBX3pcVUxtn4rYKnMH39oaOCBXMwggVvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU0PaUbhtp84Orb2YatvZkIjkZiOEwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wZgYDVR0RAQH/BFwwWoZYaHR0cHM6Ly9naXRodWIuY29tL3RyYWlsb2ZiaXRzL3JmYzg3ODUucHkvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy90YWdzL3YwLjEuMjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBUGCisGAQQBg78wAQIEB3JlbGVhc2UwNgYKKwYBBAGDvzABAwQoZDhiNGE2NDQ1ZjM4YzQ4YjkxMzdhODA5OTcwNmQ5YjgwNzMxNDZlNDAVBgorBgEEAYO/MAEEBAdyZWxlYXNlMCQGCisGAQQBg78wAQUEFnRyYWlsb2ZiaXRzL3JmYzg3ODUucHkwHgYKKwYBBAGDvzABBgQQcmVmcy90YWdzL3YwLjEuMjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20waAYKKwYBBAGDvzABCQRaDFhodHRwczovL2dpdGh1Yi5jb20vdHJhaWxvZmJpdHMvcmZjODc4NS5weS8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbEByZWZzL3RhZ3MvdjAuMS4yMDgGCisGAQQBg78wAQoEKgwoZDhiNGE2NDQ1ZjM4YzQ4YjkxMzdhODA5OTcwNmQ5YjgwNzMxNDZlNDAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwOQYKKwYBBAGDvzABDAQrDClodHRwczovL2dpdGh1Yi5jb20vdHJhaWxvZmJpdHMvcmZjODc4NS5weTA4BgorBgEEAYO/MAENBCoMKGQ4YjRhNjQ0NWYzOGM0OGI5MTM3YTgwOTk3MDZkOWI4MDczMTQ2ZTQwIAYKKwYBBAGDvzABDgQSDBByZWZzL3RhZ3MvdjAuMS4yMBkGCisGAQQBg78wAQ8ECwwJNzY4MjEzOTk3MC4GCisGAQQBg78wARAEIAweaHR0cHM6Ly9naXRodWIuY29tL3RyYWlsb2ZiaXRzMBcGCisGAQQBg78wAREECQwHMjMxNDQyMzBoBgorBgEEAYO/MAESBFoMWGh0dHBzOi8vZ2l0aHViLmNvbS90cmFpbG9mYml0cy9yZmM4Nzg1LnB5Ly5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvdGFncy92MC4xLjIwOAYKKwYBBAGDvzABEwQqDChkOGI0YTY0NDVmMzhjNDhiOTEzN2E4MDk5NzA2ZDliODA3MzE0NmU0MBcGCisGAQQBg78wARQECQwHcmVsZWFzZTBcBgorBgEEAYO/MAEVBE4MTGh0dHBzOi8vZ2l0aHViLmNvbS90cmFpbG9mYml0cy9yZmM4Nzg1LnB5L2FjdGlvbnMvcnVucy84MzUxMDU4NTAxL2F0dGVtcHRzLzEwFgYKKwYBBAGDvzABFgQIDAZwdWJsaWMwgYoGCisGAQQB1nkCBAIEfAR6AHgAdgDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAY5Y4EK+AAAEAwBHMEUCIDagfjpw1AZX374vFXGDSZgJ9Kqrcq7Tk/Us3f7nmVQ1AiEA4esGBrDhflbIUujUmYC3eUWFFBgXHfABLiSDwciTQw8wCgYIKoZIzj0EAwMDaQAwZgIxAM6gKI5vKoqcvTkv87Foq3WXNYmAhPj3qaQ5ocXQXsWzHeNWGB6lSHTG3ENyapqYBgIxAMJW9ly3JXEdI5ydHfz+GZoh1kyc0XFUPp4V4kVjnUXY+KtoQWKSPHaZMkYC/szXhg==" } ] }, "tlogEntries": [ { "logIndex": "79605083", "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1710888076", "inclusionPromise": { "signedEntryTimestamp": "MEYCIQD8ohK48/Ls8D4Qd3dQZl6geplAt0p5Sgpa1wabniB/ZgIhALsVfKCe1m2KKtaEImxijm5bO2K49NltHWafJE2a1hnr" }, "inclusionProof": { "logIndex": "75441652", "rootHash": "uAqI3id6JHPMMNUltHIKHuX1kVHpm5y7jSfnbaRO+E4=", "treeSize": "75441653", "hashes": [ "XoeIGlDW7f2lVjTlQEXPaV7szUXY2BECAEKtNA/lgfk=", "Pz5CyFQH78eikJoZuJ44Ls4R5najWJ1nKWunxb/vxeM=", "COo4wZnRb/d6zZOa7RP1euSRFb7H5EX5bYXs4HEQ0uU=", "1A4EnFDN5UCHjrJDWPuYDmY+ZLb4B+Jvis+k3ti+wjs=", "bBpWKtQryG7/tMDt9HDvKk/Fp3S+q7gTnYF56qGKMiI=", "ZR8qbYzXTNaK4SaofTZtbR0srNmOJ0Yx891OF5/G2gQ=", "7MueyMCRkh/GaluPkJl3xQFyXFq/SS9xykP299KtvS0=", "kFt/VRwfXksHcnd9vpdeifz3N16KyWQoDxAPfLlRwTA=", "gtt9e0foHZTCS9w+epNsmDWbwvX4FNV1EAg0rhxLfjg=", "BGqH+LzVuhuqCLiUvBJaB2hlsvtu2a15qq1WGw6mG44=", "OeS7D4kPES7ChE7kWSEmhbAMqBcKVj/z8/afMK4Y3pI=", "JtjqvAqFyXXYjWlZfDzElHpEzdBjsz1LmGFJuYx0kTU=", "s/ZIVcfcD4/nuZwUtQf4ydGsIAkGTPTzk3b0zhUC95k=", "YU1jZY/fp5tJdGF/i+/7ez8107O4/lOUp7acMPFEaOA=", "7Z18YLBAvejEV4nJHIKoks/xlijnhR005qTW2w4QtHg=", "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8=" ], "checkpoint": { "envelope": "rekor.sigstore.dev - 2605736670972794746\n75441653\nuAqI3id6JHPMMNUltHIKHuX1kVHpm5y7jSfnbaRO+E4=\n\n\u2014 rekor.sigstore.dev wNI9ajBGAiEA5perJLLm94gCQOQT5/vO29OXWNZ1SoengZDZ/U6vsOUCIQDBL0BIkCjWGR6V622znnVpXF5D1g0jPgajBlHh8uSc8g==\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjNGU5MmU5ZWNjODI4YmVmMmFhN2RiYTFkZThhYzk4MzUxMWY3NTMyYTBkZjExYzc3MGQzOTA5OWEyNWNmMjAxIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNlSDZFM01wWm5nV0E2UlBnOEhBbC9aNzY0aFRGWXljTnlGM1IrbVBUU2JBSWhBUGdNUzhxQk04bENFVTJYVzc2NW15TU16Mnp1eXU5aVRGNDBQSCtYWmxKUSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVZDZla05EUW14VFowRjNTVUpCWjBsVlRUSTVZblpaYTNKRVMyNUNWbHB0Vm1Wc2IxUlZUV3hhY1U1WmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UlRWTmFra3dUVlJGTVZkb1kwNU5hbEYzVFhwRk5VMXFTVEZOVkVVeFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVV4Y1RoM2JYQnRTekIyWlhORFJEQTFXa1V4YnpWS2VYVXJaeTlEZEV4YVRGaE9SVm9LYVVsdmJXZ3hhbkYxVUUxRFduSm9iRkJrVDJaNlVYZHpLMFVyU1ZWQ1dETndZMVpWZUhSdU5ISlpTMjVOU0RNNWIyRlBRMEpZVFhkbloxWjJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlV3VUdGVkNtSm9kSEE0TkU5eVlqSlpZWFIyV210SmFtdGFhVTlGZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDFwbldVUldVakJTUVZGSUwwSkdkM2RYYjFwWllVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcFlWaFNlZ3BNTTBwdFdYcG5NMDlFVlhWalNHdDJURzFrY0dSSGFERlphVGt6WWpOS2NscHRlSFprTTAxMlkyMVdjMXBYUm5wYVV6VTFZbGQ0UVdOdFZtMWplVGt3Q2xsWFpIcE1NMWwzVEdwRmRVMXFRVFZDWjI5eVFtZEZSVUZaVHk5TlFVVkNRa04wYjJSSVVuZGplbTkyVEROU2RtRXlWblZNYlVacVpFZHNkbUp1VFhVS1dqSnNNR0ZJVm1sa1dFNXNZMjFPZG1KdVVteGlibEYxV1RJNWRFMUNWVWREYVhOSFFWRlJRbWMzT0hkQlVVbEZRak5LYkdKSFZtaGpNbFYzVG1kWlN3cExkMWxDUWtGSFJIWjZRVUpCZDFGdldrUm9hVTVIUlRKT1JGRXhXbXBOTkZsNlVUUlphbXQ0VFhwa2FFOUVRVFZQVkdOM1RtMVJOVmxxWjNkT2VrMTRDazVFV214T1JFRldRbWR2Y2tKblJVVkJXVTh2VFVGRlJVSkJaSGxhVjNoc1dWaE9iRTFEVVVkRGFYTkhRVkZSUW1jM09IZEJVVlZGUm01U2VWbFhiSE1LWWpKYWFXRllVbnBNTTBwdFdYcG5NMDlFVlhWalNHdDNTR2RaUzB0M1dVSkNRVWRFZG5wQlFrSm5VVkZqYlZadFkzazVNRmxYWkhwTU0xbDNUR3BGZFFwTmFrRTNRbWR2Y2tKblJVVkJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveENtTXlWbmxaTWpsMVpFZFdkV1JETldwaU1qQjNZVUZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbUZFUm1odlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5Xb0tZakl3ZG1SSVNtaGhWM2gyV20xS2NHUklUWFpqYlZwcVQwUmpORTVUTlhkbFV6aDFXakpzTUdGSVZtbE1NMlIyWTIxMGJXSkhPVE5qZVRsNVdsZDRiQXBaV0U1c1RHNXNkR0pGUW5sYVYxcDZURE5TYUZvelRYWmtha0YxVFZNMGVVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVXOUZTMmQzYjFwRWFHbE9SMFV5Q2s1RVVURmFhazAwV1hwUk5GbHFhM2hOZW1Sb1QwUkJOVTlVWTNkT2JWRTFXV3BuZDA1NlRYaE9SRnBzVGtSQlpFSm5iM0pDWjBWRlFWbFBMMDFCUlV3S1FrRTRUVVJYWkhCa1IyZ3hXV2t4YjJJelRqQmFWMUYzVDFGWlMwdDNXVUpDUVVkRWRucEJRa1JCVVhKRVEyeHZaRWhTZDJONmIzWk1NbVJ3WkVkb01RcFphVFZxWWpJd2RtUklTbWhoVjNoMldtMUtjR1JJVFhaamJWcHFUMFJqTkU1VE5YZGxWRUUwUW1kdmNrSm5SVVZCV1U4dlRVRkZUa0pEYjAxTFIxRTBDbGxxVW1oT2FsRXdUbGRaZWs5SFRUQlBSMGsxVFZSTk0xbFVaM2RQVkdzelRVUmFhMDlYU1RSTlJHTjZUVlJSTWxwVVVYZEpRVmxMUzNkWlFrSkJSMFFLZG5wQlFrUm5VVk5FUWtKNVdsZGFla3d6VW1oYU0wMTJaR3BCZFUxVE5IbE5RbXRIUTJselIwRlJVVUpuTnpoM1FWRTRSVU4zZDBwT2VsazBUV3BGZWdwUFZHc3pUVU0wUjBOcGMwZEJVVkZDWnpjNGQwRlNRVVZKUVhkbFlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcENtRllVbnBOUW1OSFEybHpSMEZSVVVKbk56aDNRVkpGUlVOUmQwaE5hazE0VGtSUmVVMTZRbTlDWjI5eVFtZEZSVUZaVHk5TlFVVlRRa1p2VFZkSGFEQUtaRWhDZWs5cE9IWmFNbXd3WVVoV2FVeHRUblppVXprd1kyMUdjR0pIT1cxWmJXd3dZM2s1ZVZwdFRUUk9lbWN4VEc1Q05VeDVOVzVoV0ZKdlpGZEpkZ3BrTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFprUjBadVkzazVNazFETkhoTWFrbDNUMEZaUzB0M1dVSkNRVWRFQ25aNlFVSkZkMUZ4UkVOb2EwOUhTVEJaVkZrd1RrUldiVTE2YUdwT1JHaHBUMVJGZWs0eVJUUk5SR3MxVG5wQk1scEViR2xQUkVFelRYcEZNRTV0VlRBS1RVSmpSME5wYzBkQlVWRkNaemM0ZDBGU1VVVkRVWGRJWTIxV2MxcFhSbnBhVkVKalFtZHZja0puUlVWQldVOHZUVUZGVmtKRk5FMVVSMmd3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVNR050Um5CaVJ6bHRXVzFzTUdONU9YbGFiVTAwVG5wbk1VeHVRalZNTWtacVpFZHNkbUp1VFhaamJsWjFDbU41T0RSTmVsVjRUVVJWTkU1VVFYaE1Na1l3WkVkV2RHTklVbnBNZWtWM1JtZFpTMHQzV1VKQ1FVZEVkbnBCUWtablVVbEVRVnAzWkZkS2MyRlhUWGNLWjFsdlIwTnBjMGRCVVZGQ01XNXJRMEpCU1VWbVFWSTJRVWhuUVdSblJHUlFWRUp4ZUhOalVrMXRUVnBJYUhsYVducGpRMjlyY0dWMVRqUTRjbVlyU0FwcGJrdEJUSGx1ZFdwblFVRkJXVFZaTkVWTEswRkJRVVZCZDBKSVRVVlZRMGxFWVdkbWFuQjNNVUZhV0RNM05IWkdXRWRFVTFwblNqbExjWEpqY1RkVUNtc3ZWWE16WmpkdWJWWlJNVUZwUlVFMFpYTkhRbkpFYUdac1lrbFZkV3BWYlZsRE0yVlZWMFpHUW1kWVNHWkJRa3hwVTBSM1kybFVVWGM0ZDBObldVa0tTMjlhU1hwcU1FVkJkMDFFWVZGQmQxcG5TWGhCVFRablMwazFka3R2Y1dOMlZHdDJPRGRHYjNFelYxaE9XVzFCYUZCcU0zRmhVVFZ2WTFoUldITlhlZ3BJWlU1WFIwSTJiRk5JVkVjelJVNTVZWEJ4V1VKblNYaEJUVXBYT1d4NU0wcFlSV1JKTlhsa1NHWjZLMGRhYjJneGEzbGpNRmhHVlZCd05GWTBhMVpxQ201VldGa3JTM1J2VVZkTFUxQklZVnBOYTFsREwzTjZXR2huUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "xOkunsyCi+8qp9uh3orJg1EfdTKg3xHHcNOQmaJc8gE=" }, "signature": "MEYCIQCeH6E3MpZngWA6RPg8HAl/Z764hTFYycNyF3R+mPTSbAIhAPgMS8qBM8lCEU2XW765myMMz2zuyu9iTF40PH+XZlJQ" } } ================================================ FILE: test/assets/bundle_v3_no_signed_time.txt ================================================ DO NOT MODIFY ME! this is the input for bundle_v3_no_signed_time, which ensures clients reject bundles that don't have a source of signed time. DO NOT MODIFY ME! ================================================ FILE: test/assets/bundle_v3_no_signed_time.txt.sigstore.json ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { "rawBytes": "MIIC1DCCAlugAwIBAgIUXgKINnY7rbT5gHmj9yeiZXGg3rkwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMjEwMjE0MTI1WhcNMjQxMjEwMjE1MTI1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4ul4I08UFGizCla6qRUGFiwEPNsFRnvBPDvQ4ViJ+Q83HOlYWWxCAjoJpGd9FWtyxTPKDsG0n4t6Mr+jSwz22KOCAXowggF2MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUZ7cNLqQlnKAXnf6jmb9cv70dppgwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABk7KFEeIAAAQDAEgwRgIhAOeS6rR2aksHhN9Rxbx+ANuAlXhP4vTPKMLBHd6JAm4lAiEAx+/kzKJ2SxSCAYm582jKeAa1LCVmUaO85FO2WTV7MYEwCgYIKoZIzj0EAwMDZwAwZAIwDXrVAPgutWZWPfE3QWy/4gG/PbMbYUfqNsEpQEeMm8GeraZN3zffzw16FFhWsMbXAjApxDNgKvmztHOKStyvmOXPiJCixzx/gLFbhVn7Q+qY6vjC83B0XgPsyQ2T0i8Ldzg=" }, "tlogEntries": [ { "logIndex": "154562758", "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1733866885", "inclusionProof": { "logIndex": "32658496", "rootHash": "IbC2+n9aYhFlm5nFwkp+j7/Hc9XuYWxyE5OlXIoIijY=", "treeSize": "32658497", "hashes": [ "CVvwGSdkZ5FUDnltf3Me3nXyco4G9mwTsYbIxz0RS+U=", "DJrEpKAKhEPhZ5aKvlaRImFebTv5tc17rsfOkhSS6fY=", "tsYfO+hUsl4KKY+qsPx/k4NzOzE5zWRsc4Ufgn4oh/U=", "ZjSpDQt5kIQfJd6B/BDNWLRhYOGwnlxE6pT4JJaiD5s=", "OMoiMVnwD3sG6Cc6HCg+ySmqBAH1nn0mA5+tjFxiyeg=", "gSWKL2k1ZGZm45C8hSdNwWan8qOrszl5X7Ws56h+FVM=", "R7hO1X+KgSw8Oojd8i2+G3BzBYztkRBE6LpYSXPg33U=", "oOecFfN3YqDOkbijS/ej1WF5Da/Gt/AZNhbwE9uoOE8=", "4lUF0YOu9XkIDXKXA0wMSzd6VeDY3TZAgmoOeWmS2+Y=", "gf+9m552B3PnkWnO0o4KdVvjcT3WVHLrCbf1DoVYKFw=" ], "checkpoint": { "envelope": "rekor.sigstore.dev - 1193050959916656506\n32658497\nIbC2+n9aYhFlm5nFwkp+j7/Hc9XuYWxyE5OlXIoIijY=\n\n\u2014 rekor.sigstore.dev wNI9ajBGAiEAgjFaCZlVvHUnDgxLf+4XjN6ahWNkkKh9QFTOqHBpyw4CIQDmy4JQs+2BKtvheo/HQogyhh5EYGYZeBDdRvyyX1fg+w==\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjYTZkZTk5YTExZDNkMzgwNTZkODM4YzdkYzlhMjNhMTFhMGM4MWJjYWNlMGQxMWVhYTMwMWEyZmZiNDgyYzQyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUMzc2pYZVZoTHRqbE13dG0yRE5CYVdVaFBWOVJ1U1dsWW1EcHQzRzFQVW5RSWhBUElxRHUwTVkza1FtelE2QmswS2VSTW5mQ3Y0VVdEVU5jclRnN0cyYjdzTCIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4MVowRjNTVUpCWjBsVldHZExTVTV1V1RkeVlsUTFaMGh0YWpsNVpXbGFXRWRuTTNKcmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFxUlhkTmFrVXdUVlJKTVZkb1kwNU5hbEY0VFdwRmQwMXFSVEZOVkVreFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUwZFd3MFNUQTRWVVpIYVhwRGJHRTJjVkpWUjBacGQwVlFUbk5HVW01MlFsQkVkbEVLTkZacFNpdFJPRE5JVDJ4WlYxZDRRMEZxYjBwd1IyUTVSbGQwZVhoVVVFdEVjMGN3YmpSME5rMXlLMnBUZDNveU1rdFBRMEZZYjNkblowWXlUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZhTjJOT0NreHhVV3h1UzBGWWJtWTJhbTFpT1dOMk56QmtjSEJuZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwZDFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpsQ1NITkJaVkZDTTBGT01EbE5SM0pIZUhoRmVWbDRhMlZJU214dVRuZExhVk5zTmpRemFubDBMelJsUzJOdlFYWkxaVFpQUVVGQlFncHJOMHRHUldWSlFVRkJVVVJCUldkM1VtZEphRUZQWlZNMmNsSXlZV3R6U0doT09WSjRZbmdyUVU1MVFXeFlhRkEwZGxSUVMwMU1Ra2hrTmtwQmJUUnNDa0ZwUlVGNEt5OXJla3RLTWxONFUwTkJXVzAxT0RKcVMyVkJZVEZNUTFadFZXRlBPRFZHVHpKWFZGWTNUVmxGZDBObldVbExiMXBKZW1vd1JVRjNUVVFLV25kQmQxcEJTWGRFV0hKV1FWQm5kWFJYV2xkUVprVXpVVmQ1THpSblJ5OVFZazFpV1ZWbWNVNXpSWEJSUldWTmJUaEhaWEpoV2s0emVtWm1lbmN4TmdwR1JtaFhjMDFpV0VGcVFYQjRSRTVuUzNadGVuUklUMHRUZEhsMmJVOVlVR2xLUTJsNGVuZ3ZaMHhHWW1oV2JqZFJLM0ZaTm5acVF6Z3pRakJZWjFCekNubFJNbFF3YVRoTVpIcG5QUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=" } ], "timestampVerificationData": {} }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "ym3pmhHT04BW2DjH3JojoRoMgbys4NEeqjAaL/tILEI=" }, "signature": "MEYCIQC3sjXeVhLtjlMwtm2DNBaWUhPV9RuSWlYmDpt3G1PUnQIhAPIqDu0MY3kQmzQ6Bk0KeRMnfCv4UWDUNcrTg7G2b7sL" } } ================================================ FILE: test/assets/c.txt ================================================ DO NOT MODIFY ME! this is "c.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/c.txt.crt ================================================ -----BEGIN CERTIFICATE----- MIIDwTCCA0igAwIBAgIUdXPCI40ren/SEkqxmHcCc6lIV7MwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjIxMTAzMDc0NTM1WhcNMjIxMTAzMDc1NTM1WjAAMHYwEAYH KoZIzj0CAQYFK4EEACIDYgAELVUlqi4FjTw4mHzuyE8sOsK6mVvzOTv0EX7ot+aZ ftaf+ato9xuemqA69qARscFPwG15It1F9PVdKUOeJkTPjZC+lRHNAIeamJpilskz xqR6fisI7q72zHY8OhgMnSSHo4ICSjCCAkYwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBREb7Dfm1g8gILV3K9rT9WSF7GnzzAf BgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDBmBgNVHREBAf8EXDBahlho dHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtcHl0aG9uLy5naXRo dWIvd29ya2Zsb3dzL2NpLnltbEByZWZzL3B1bGwvMjg4L21lcmdlMDkGCisGAQQB g78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5j b20wGgYKKwYBBAGDvzABAgQMcHVsbF9yZXF1ZXN0MDYGCisGAQQBg78wAQMEKDNi ZTcyMzU2ZWY0NTE3YmI4ZTUwZjI5Njg4N2Y5YzU3ODZmOTAzMTYwEAYKKwYBBAGD vzABBAQCQ0kwJgYKKwYBBAGDvzABBQQYc2lnc3RvcmUvc2lnc3RvcmUtcHl0aG9u MCEGCisGAQQBg78wAQYEE3JlZnMvcHVsbC8yODgvbWVyZ2UwgYoGCisGAQQB1nkC BAIEfAR6AHgAdgArMLzcaIjJ4uHYJiledB9IOTGWAvKcM8teQ0D+sqyGegAAAYQ8 c9igAAAEAwBHMEUCIQCn/JSbLxs0ds3Nycn0yINUQABeltbAmcYDFEn/sdm50gIg fm4lKdhXJoWHJRC8IS7MxYI3yR/oNzX6dntuqpHJ24YwCgYIKoZIzj0EAwMDZwAw ZAIwE0F3B/HgHn+ov6axOY0TMR/hv2DUVlC3qkGBQEEMtglf5qtT+a9g7aQ5g4pG of+JAjB+qUeUdSAyGPDK+5Ti6aROy0oAbwl+B3bH7QmmZ/i5M++PXIW4l4lcuAmA UkjTgLw= -----END CERTIFICATE----- ================================================ FILE: test/assets/c.txt.sig ================================================ MGUCMAQYRaYOdZEOT3C3WP22sC9+2euiFGYbC4VNefWVL31+MAL7oKMWsHsBwh1ngjTZHAIxALuUf+mzlACBqYUSTTwl3LFIGUGl8g3Z6wkTMsqdI1NrtHj0rVpcWA1DIO4GhGOM5w== ================================================ FILE: test/assets/integration/Python-3.12.5.tgz.sigstore ================================================ {"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": {"x509CertificateChain": {"certificates": [{"rawBytes": "MIIC5zCCAm2gAwIBAgIUJlhDDqj05f6TwIEKO4YUQ+JeMUgwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwODA2MjAzMjQ3WhcNMjQwODA2MjA0MjQ3WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEyfxCuMuSwrq27CDuXVog75EfL9WfcuY9Z2NmxikgeF8oMEG4mMN+ULqfNR/uM9+XzT5ideXYPYp+I9Sj/hDFv4G7dk1YYgvySUqrY7uxeUYvVSk+Y3ZiPgk9ADu6wPAzo4IBbzCCAWswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBRXq80OR1/j1OhcQlF00SLIgjjKgDAfBgNVHSMEGDAWgBTf0+nPViQRlvmo2OkoVaLGLhhkPzAfBgNVHREBAf8EFTATgRF0aG9tYXNAcHl0aG9uLm9yZzApBgorBgEEAYO/MAEBBBtodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20wKwYKKwYBBAGDvzABCAQdDBtodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20wgYoGCisGAQQB1nkCBAIEfAR6AHgAdgDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAZEpZPIRAAAEAwBHMEUCIQDyXwfd7XnVIidGsF1oawebvXpVrlKE5xaGoywy7KU+XQIgWiFoQP4yq0cZmuY3BWBSvjXC2LFHOt75Bgda6wN40mwwCgYIKoZIzj0EAwMDaAAwZQIwbUsZO2Go1XXJx31LtqG2wA6W8yQUMzoieEy6aSF5h9Ka3G80vJnlGIu1Gv1BgGSuAjEA8I8O6Nb7pGpejOSHEb+eKFBjHJzsAYhRc4+QaVSi2poc9UMvg01qfTtXyE/HsNgw"}]}, "tlogEntries": [{"logIndex": "118981923", "logId": {"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1722976367", "inclusionPromise": {"signedEntryTimestamp": "MEUCIFHZzeCjijPmhyFe2nM04kIAJ7MUxBZUE5/dDN2az/YYAiEApLjBB/nZJJHYoMXhg+VfKOPmRNymdDQevt390XU6xoo="}, "inclusionProof": {"logIndex": "114818492", "rootHash": "IqAkWiiNkCTFxyYb94s81eNqaapA73SgxBxd06iPI04=", "treeSize": "114818493", "hashes": ["PMN+wGyFObrmIvP3UuG8F/K3r+S5gnVUNjTG9KRxSQI=", "IbBdNH70ZqaY+VA0Gox1yc/e7rTLDAr00GFLtAS1mM0=", "d9hP0b+P5gvyMADKIkgpYQfvzecgmGRsUAAfRXSkCvQ=", "0mWfN8v15Z2C5/2mwHGp1Tns3g82mm+8tcRMCmSlTkQ=", "N/jfjW9aFr+UzHBai8+y+VBVG5BztJO/AZcC+BxllRg=", "aVnjeQ4AARM1lia/y4Z6qLrK9b7yLU9GvzYjrhVNIGQ=", "/oczRbnX0wVoMcxf3FonUslk7JCszDsgFwdWN3hQ/PI=", "bJQEErUPH5I1mbnua8mOhyl0xwcbcK3SE1ktgx9zIZc=", "mJjriUsaYb3cYi8BAKBoYkXOb60BV9QLvVl4JkCof8s=", "FuqiuF+HGbxEPfTq5V1LEOD2xEkbOhSTHhh9OgesRec=", "gdYky8OkC3TR65e8i+N+u+FW8WwVOWv3ReiEdspNMoU=", "8QWire253mh3dyplsqOeYFI2Ar7vM6tDRPFjeMYLxck=", "uQRyyLzWiHmeVVM6L4XonE+3Lh8nQrzaUFXwRnObrjE=", "lvYqunhigwQrJ1cNg7lMmilqxS8D8HoDJPLndmoaKoM=", "1uSClB8CJleRshjxptJIRvzgY8fg8XITEtJZiU2Exwo=", "v7N3pwo5/dDC9hrWE31X4X+pIwTlvQXBlFvUC/xjjdc=", "yPZFEKyq0Jj5sObbCwB/LMHlcgQl8ux2d2IkRYWLIt8=", "ndmjFxe89oJp4z+fXcLQM1BmC+7Sp8m8VMkNIafNhYk=", "a6kLnwN4nPldqWq4OoO6Mz25ZQx1TaLMF0IbMSMVduQ=", "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8="]}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzOGRjNGUyYzI2MWQ0OWM2NjExOTYwNjZlZGJmYjcwZmRiMTZiZTRhNzljYzgyMjBjMjI0ZGZlYjU2MzZkNDA1In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HVUNNQ1YrbnlnYlJ3RUpkRENJNk9vbCs1R0dzL2RidUdOTzdQU3dBMjl4aHBPSjArQUJRdmwxMnBHekszdXp1bEl6aGdJeEFLbWVDSFYvbUs1cGxlTi9zTHFGaWRobGE5VGFVbXNZaFp5SUJJaCs4NmVydy9GTHBQWGI1bloxOEFXTHJGUWZNQT09IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNMWVrTkRRVzB5WjBGM1NVSkJaMGxWU214b1JFUnhhakExWmpaVWQwbEZTMDgwV1ZWUkswcGxUVlZuZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmQwOUVRVEpOYWtGNlRXcFJNMWRvWTA1TmFsRjNUMFJCTWsxcVFUQk5hbEV6VjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlhsbWVFTjFUWFZUZDNKeE1qZERSSFZZVm05bk56VkZaa3c1VjJaamRWazVXakpPYlhocGEyY0taVVk0YjAxRlJ6UnRUVTRyVlV4eFprNVNMM1ZOT1N0WWVsUTFhV1JsV0ZsUVdYQXJTVGxUYWk5b1JFWjJORWMzWkdzeFdWbG5kbmxUVlhGeVdUZDFlQXBsVlZsMlZsTnJLMWt6V21sUVoyczVRVVIxTm5kUVFYcHZORWxDWW5wRFEwRlhjM2RFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pTV0hFNE1FOVNNUzlxTVU5b1kxRnNSakF3VTB4SloycHFTMmRFUVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpVWmpBcmJsQldhVkZTYkhadGJ6SlBhMjlXWVV4SFRHaG9hMUI2UVdaQ1owNVdTRkpGUWtGbU9FVkdWRUZVWjFKR01BcGhSemwwV1ZoT1FXTkliREJoUnpsMVRHMDVlVnA2UVhCQ1oyOXlRbWRGUlVGWlR5OU5RVVZDUWtKMGIyUklVbmRqZW05MlRESkdhbGt5T1RGaWJsSjZDa3h0WkhaaU1tUnpXbE0xYW1JeU1IZExkMWxMUzNkWlFrSkJSMFIyZWtGQ1EwRlJaRVJDZEc5a1NGSjNZM3B2ZGt3eVJtcFpNamt4WW01U2VreHRaSFlLWWpKa2MxcFROV3BpTWpCM1oxbHZSME5wYzBkQlVWRkNNVzVyUTBKQlNVVm1RVkkyUVVoblFXUm5SR1JRVkVKeGVITmpVazF0VFZwSWFIbGFXbnBqUXdwdmEzQmxkVTQwT0hKbUswaHBia3RCVEhsdWRXcG5RVUZCV2tWd1dsQkpVa0ZCUVVWQmQwSklUVVZWUTBsUlJIbFlkMlprTjFodVZrbHBaRWR6UmpGdkNtRjNaV0oyV0hCV2NteExSVFY0WVVkdmVYZDVOMHRWSzFoUlNXZFhhVVp2VVZBMGVYRXdZMXB0ZFZrelFsZENVM1pxV0VNeVRFWklUM1EzTlVKblpHRUtObmRPTkRCdGQzZERaMWxKUzI5YVNYcHFNRVZCZDAxRVlVRkJkMXBSU1hkaVZYTmFUekpIYnpGWVdFcDRNekZNZEhGSE1uZEJObGM0ZVZGVlRYcHZhUXBsUlhrMllWTkdOV2c1UzJFelJ6Z3dka3B1YkVkSmRURkhkakZDWjBkVGRVRnFSVUU0U1RoUE5rNWlOM0JIY0dWcVQxTklSV0lyWlV0R1FtcElTbnB6Q2tGWmFGSmpOQ3RSWVZaVGFUSndiMk01VlUxMlp6QXhjV1pVZEZoNVJTOUljMDVuZHdvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19"}]}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "ONxOLCYdScZhGWBm7b+3D9sWvkp5zIIgwiTf61Y21AU="}, "signature": "MGUCMCV+nygbRwEJdDCI6Ool+5GGs/dbuGNO7PSwA29xhpOJ0+ABQvl12pGzK3uzulIzhgIxAKmeCHV/mK5pleN/sLqFidhla9TaUmsYhZyIBIh+86erw/FLpPXb5nZ18AWLrFQfMA=="}} ================================================ FILE: test/assets/integration/a.txt ================================================ DO NOT MODIFY ME! this is "a.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/integration/attest/slsa_predicate_v0_2.json ================================================ { "builder": { "id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.0.0" }, "buildType": "https://github.com/slsa-framework/slsa-github-generator/generic@v1", "invocation": { "configSource": { "uri": "git+https://github.com/sigstore/sigstore-python@refs/tags/v3.2.0", "digest": { "sha1": "fc29ec190575ae345cea23f0953b64ca6f2ab8ba" }, "entryPoint": ".github/workflows/release.yml" }, "parameters": {}, "environment": { "github_actor": "woodruffw", "github_actor_id": "3059210", "github_base_ref": "", "github_event_name": "release", "github_event_payload": { "action": "published", "enterprise": { "avatar_url": "https://avatars.githubusercontent.com/b/102459?v=4", "created_at": "2023-12-08T05:54:26Z", "description": "Open Source Security Foundation (OpenSSF)", "html_url": "https://github.com/enterprises/openssf", "id": 102459, "name": "Open Source Security Foundation", "node_id": "E_kgDOAAGQOw", "slug": "openssf", "updated_at": "2024-01-06T00:47:02Z", "website_url": "https://openssf.org/" }, "organization": { "avatar_url": "https://avatars.githubusercontent.com/u/71096353?v=4", "description": "Software Supply Chain Security", "events_url": "https://api.github.com/orgs/sigstore/events", "hooks_url": "https://api.github.com/orgs/sigstore/hooks", "id": 71096353, "issues_url": "https://api.github.com/orgs/sigstore/issues", "login": "sigstore", "members_url": "https://api.github.com/orgs/sigstore/members{/member}", "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxMDk2MzUz", "public_members_url": "https://api.github.com/orgs/sigstore/public_members{/member}", "repos_url": "https://api.github.com/orgs/sigstore/repos", "url": "https://api.github.com/orgs/sigstore" }, "release": { "assets": [], "assets_url": "https://api.github.com/repos/sigstore/sigstore-python/releases/170913493/assets", "author": { "avatar_url": "https://avatars.githubusercontent.com/u/3059210?v=4", "events_url": "https://api.github.com/users/woodruffw/events{/privacy}", "followers_url": "https://api.github.com/users/woodruffw/followers", "following_url": "https://api.github.com/users/woodruffw/following{/other_user}", "gists_url": "https://api.github.com/users/woodruffw/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/woodruffw", "id": 3059210, "login": "woodruffw", "node_id": "MDQ6VXNlcjMwNTkyMTA=", "organizations_url": "https://api.github.com/users/woodruffw/orgs", "received_events_url": "https://api.github.com/users/woodruffw/received_events", "repos_url": "https://api.github.com/users/woodruffw/repos", "site_admin": false, "starred_url": "https://api.github.com/users/woodruffw/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/woodruffw/subscriptions", "type": "User", "url": "https://api.github.com/users/woodruffw" }, "body": "### Added\n\n* API: `models.Bundle.BundleType` is now a public API\n ([#1089](https://github.com/sigstore/sigstore-python/pull/1089))\n\n* CLI: The `sigstore plumbing` subcommand hierarchy has been added. This\n hierarchy is for *developer-only* interactions, such as fixing malformed\n Sigstore bundles. These subcommands are **not considered stable until\n explicitly documented as such**.\n ([#1089](https://github.com/sigstore/sigstore-python/pull/1089))\n\n### Changed\n\n* CLI: The default console logger now emits to `stderr`, rather than `stdout`\n ([#1089](https://github.com/sigstore/sigstore-python/pull/1089))\n\n", "created_at": "2024-08-19T17:14:19Z", "draft": false, "html_url": "https://github.com/sigstore/sigstore-python/releases/tag/v3.2.0", "id": 170913493, "name": "v3.2.0", "node_id": "RE_kwDOGq85Ts4KL-7V", "prerelease": false, "published_at": "2024-08-19T17:15:11Z", "tag_name": "v3.2.0", "tarball_url": "https://api.github.com/repos/sigstore/sigstore-python/tarball/v3.2.0", "target_commitish": "main", "upload_url": "https://uploads.github.com/repos/sigstore/sigstore-python/releases/170913493/assets{?name,label}", "url": "https://api.github.com/repos/sigstore/sigstore-python/releases/170913493", "zipball_url": "https://api.github.com/repos/sigstore/sigstore-python/zipball/v3.2.0" }, "repository": { "allow_forking": true, "archive_url": "https://api.github.com/repos/sigstore/sigstore-python/{archive_format}{/ref}", "archived": false, "assignees_url": "https://api.github.com/repos/sigstore/sigstore-python/assignees{/user}", "blobs_url": "https://api.github.com/repos/sigstore/sigstore-python/git/blobs{/sha}", "branches_url": "https://api.github.com/repos/sigstore/sigstore-python/branches{/branch}", "clone_url": "https://github.com/sigstore/sigstore-python.git", "collaborators_url": "https://api.github.com/repos/sigstore/sigstore-python/collaborators{/collaborator}", "comments_url": "https://api.github.com/repos/sigstore/sigstore-python/comments{/number}", "commits_url": "https://api.github.com/repos/sigstore/sigstore-python/commits{/sha}", "compare_url": "https://api.github.com/repos/sigstore/sigstore-python/compare/{base}...{head}", "contents_url": "https://api.github.com/repos/sigstore/sigstore-python/contents/{+path}", "contributors_url": "https://api.github.com/repos/sigstore/sigstore-python/contributors", "created_at": "2022-01-13T17:29:37Z", "custom_properties": {}, "default_branch": "main", "deployments_url": "https://api.github.com/repos/sigstore/sigstore-python/deployments", "description": "A Sigstore client written in Python", "disabled": false, "downloads_url": "https://api.github.com/repos/sigstore/sigstore-python/downloads", "events_url": "https://api.github.com/repos/sigstore/sigstore-python/events", "fork": false, "forks": 41, "forks_count": 41, "forks_url": "https://api.github.com/repos/sigstore/sigstore-python/forks", "full_name": "sigstore/sigstore-python", "git_commits_url": "https://api.github.com/repos/sigstore/sigstore-python/git/commits{/sha}", "git_refs_url": "https://api.github.com/repos/sigstore/sigstore-python/git/refs{/sha}", "git_tags_url": "https://api.github.com/repos/sigstore/sigstore-python/git/tags{/sha}", "git_url": "git://github.com/sigstore/sigstore-python.git", "has_discussions": false, "has_downloads": true, "has_issues": true, "has_pages": true, "has_projects": true, "has_wiki": false, "homepage": "https://pypi.org/p/sigstore", "hooks_url": "https://api.github.com/repos/sigstore/sigstore-python/hooks", "html_url": "https://github.com/sigstore/sigstore-python", "id": 447691086, "is_template": false, "issue_comment_url": "https://api.github.com/repos/sigstore/sigstore-python/issues/comments{/number}", "issue_events_url": "https://api.github.com/repos/sigstore/sigstore-python/issues/events{/number}", "issues_url": "https://api.github.com/repos/sigstore/sigstore-python/issues{/number}", "keys_url": "https://api.github.com/repos/sigstore/sigstore-python/keys{/key_id}", "labels_url": "https://api.github.com/repos/sigstore/sigstore-python/labels{/name}", "language": "Python", "languages_url": "https://api.github.com/repos/sigstore/sigstore-python/languages", "license": { "key": "other", "name": "Other", "node_id": "MDc6TGljZW5zZTA=", "spdx_id": "NOASSERTION", "url": null }, "merges_url": "https://api.github.com/repos/sigstore/sigstore-python/merges", "milestones_url": "https://api.github.com/repos/sigstore/sigstore-python/milestones{/number}", "mirror_url": null, "name": "sigstore-python", "node_id": "R_kgDOGq85Tg", "notifications_url": "https://api.github.com/repos/sigstore/sigstore-python/notifications{?since,all,participating}", "open_issues": 28, "open_issues_count": 28, "owner": { "avatar_url": "https://avatars.githubusercontent.com/u/71096353?v=4", "events_url": "https://api.github.com/users/sigstore/events{/privacy}", "followers_url": "https://api.github.com/users/sigstore/followers", "following_url": "https://api.github.com/users/sigstore/following{/other_user}", "gists_url": "https://api.github.com/users/sigstore/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/sigstore", "id": 71096353, "login": "sigstore", "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxMDk2MzUz", "organizations_url": "https://api.github.com/users/sigstore/orgs", "received_events_url": "https://api.github.com/users/sigstore/received_events", "repos_url": "https://api.github.com/users/sigstore/repos", "site_admin": false, "starred_url": "https://api.github.com/users/sigstore/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/sigstore/subscriptions", "type": "Organization", "url": "https://api.github.com/users/sigstore" }, "private": false, "pulls_url": "https://api.github.com/repos/sigstore/sigstore-python/pulls{/number}", "pushed_at": "2024-08-19T17:14:57Z", "releases_url": "https://api.github.com/repos/sigstore/sigstore-python/releases{/id}", "size": 1835, "ssh_url": "git@github.com:sigstore/sigstore-python.git", "stargazers_count": 219, "stargazers_url": "https://api.github.com/repos/sigstore/sigstore-python/stargazers", "statuses_url": "https://api.github.com/repos/sigstore/sigstore-python/statuses/{sha}", "subscribers_url": "https://api.github.com/repos/sigstore/sigstore-python/subscribers", "subscription_url": "https://api.github.com/repos/sigstore/sigstore-python/subscription", "svn_url": "https://github.com/sigstore/sigstore-python", "tags_url": "https://api.github.com/repos/sigstore/sigstore-python/tags", "teams_url": "https://api.github.com/repos/sigstore/sigstore-python/teams", "topics": [ "codesigning", "python", "security", "supply-chain" ], "trees_url": "https://api.github.com/repos/sigstore/sigstore-python/git/trees{/sha}", "updated_at": "2024-08-19T17:14:23Z", "url": "https://api.github.com/repos/sigstore/sigstore-python", "visibility": "public", "watchers": 219, "watchers_count": 219, "web_commit_signoff_required": true }, "sender": { "avatar_url": "https://avatars.githubusercontent.com/u/3059210?v=4", "events_url": "https://api.github.com/users/woodruffw/events{/privacy}", "followers_url": "https://api.github.com/users/woodruffw/followers", "following_url": "https://api.github.com/users/woodruffw/following{/other_user}", "gists_url": "https://api.github.com/users/woodruffw/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/woodruffw", "id": 3059210, "login": "woodruffw", "node_id": "MDQ6VXNlcjMwNTkyMTA=", "organizations_url": "https://api.github.com/users/woodruffw/orgs", "received_events_url": "https://api.github.com/users/woodruffw/received_events", "repos_url": "https://api.github.com/users/woodruffw/repos", "site_admin": false, "starred_url": "https://api.github.com/users/woodruffw/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/woodruffw/subscriptions", "type": "User", "url": "https://api.github.com/users/woodruffw" } }, "github_head_ref": "", "github_ref": "refs/tags/v3.2.0", "github_ref_type": "tag", "github_repository_id": "447691086", "github_repository_owner": "sigstore", "github_repository_owner_id": "71096353", "github_run_attempt": "1", "github_run_id": "10457864437", "github_run_number": "61", "github_sha1": "fc29ec190575ae345cea23f0953b64ca6f2ab8ba" } }, "metadata": { "buildInvocationId": "10457864437-1", "completeness": { "parameters": true, "environment": false, "materials": false }, "reproducible": false }, "materials": [ { "uri": "git+https://github.com/sigstore/sigstore-python@refs/tags/v3.2.0", "digest": { "sha1": "fc29ec190575ae345cea23f0953b64ca6f2ab8ba" } } ] } ================================================ FILE: test/assets/integration/attest/slsa_predicate_v1_0.json ================================================ { "buildDefinition": { "buildType": "https://actions.github.io/buildtypes/workflow/v1", "externalParameters": { "workflow": { "ref": "refs/tags/1.21.0", "repository": "https://github.com/octo-org/octo-repo", "path": ".github/workflows/ci.yaml" } }, "internalParameters": { "github": { "event_name": "push", "repository_id": "000000000", "repository_owner_id": "0000000", "runner_environment": "github-hosted" } }, "resolvedDependencies": [ { "uri": "git+https://github.com/octo-org/octo-repo@refs/tags/1.21.0", "digest": { "gitCommit": "1ac93ce21ee526b36fd154b9058d97dfaa424c50" } } ] }, "runDetails": { "builder": { "id": "https://github.com/octo-org/octo-repo/.github/workflows/docker.yaml@refs/heads/development" }, "metadata": { "invocationId": "https://github.com/octo-org/octo-repo/actions/runs/10313983218/attempts/2" } } } ================================================ FILE: test/assets/integration/b.txt ================================================ DO NOT MODIFY ME! this is "b.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/integration/bundle_v3.txt ================================================ DO NOT MODIFY ME! this is the input for bundle_v3, which tests support for "v3" bundles. DO NOT MODIFY ME! ================================================ FILE: test/assets/integration/bundle_v3.txt.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { "rawBytes": "MIIC1DCCAlqgAwIBAgIUO3tlVbLtvLPp+6zGOtep1SPkRigwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDAyMTkxOTA5WhcNMjQwNDAyMTkyOTA5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENdrfpgNU1Rjmz+j65rpJWKc08ruKYy4FX7nmmOnbauFZimsQXrdyDSXKNRtEXX4X3t/Amt+euwPDBh+eq7BCnqOCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUGRlBhD0wvzAfLb2dMWOgPrrJuRkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjqBAQZ4AAAQDAEcwRQIgeWUmtnD0MFUl5kkX7nbMdLWCsDGIPzdIlN+WaZF0TmkCIQC7+31saqrFe9RmduVZ2dxXhUPrajltuSDHb1vSGOcuHjAKBggqhkjOPQQDAwNoADBlAjEAn2+uuLHsnH9Db7zkIdF65YhiXbgMMF//iHc+B/QETK0HYVcOPTK3p46FUzXFD6xrAjAO2hrkfjBKANKjJJxHV3FVrtS+TR0GCP0HzC3D7Br95TXzfO7+j4Dd8/N/aAr6Ibs=" }, "tlogEntries": [ { "logIndex": "25915956", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1712085549", "inclusionPromise": { "signedEntryTimestamp": "MEYCIQD2KXW1NppUhkPPzGR8NrUIyN+MzZSSqGZQO7CzvhSnYgIhAO9AHzjbsr1AHXRHmEpdPZcoFHEwwMTgfqwjoOXVMmqN" }, "inclusionProof": { "logIndex": "25901137", "rootHash": "iGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=", "treeSize": "25901138", "hashes": [ "UHUr+lvxENI+G902oEsFW5ovQILgqO9mUWWxvvwHZZc=", "IcMBsbH3GRW8FX2CiL/ljMb45vzmENmhp5Yp/7IW998=", "SxC6nr0zP+a6kWb6nO2fmEtz8BYAbqEXc+dsqGLdRPM=", "sppZRSz/vdeLlavgvICrXHLeReMTJw98bs9HJ0I8WnE=", "c8lCSuBS6MzrRnt6OiyYjqhTyxUI/22gpVB7dblfDis=", "eJk64J6cMpIljPSX/72kH0kiIeElyypQm5vJ2gMMyHw=", "hbIK+jmAwQjU7Yi3iKvnfR1u7GNippk7QsRwJXIuRaw=", "tpHWIEB2vNU5ZmC68dj1Hh9cwQK083ozogA6zJ3cJ8A=", "arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=", "Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=", "rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8050909264565447525\n25901138\niGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiAMJJLbnNOnmizMbVBz9/A/qnMK15BudWoZkuE+obD6CAIhAJf6A3h2iOpuhz/duEhG3fbAQG9PXln4wXPHFBT5wT1a\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ZTZhZTlkZTU4YzExNzdiZWE2MTViNGZjYmZiMmZkNjg4ZThjNGI1MWMyZTU2YjZhMzhlODE3ODMzZWMyNGEyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRFFTSmk5YWVydFFobVQrY2UxaktOZENlNEtTY3NLR3E5ZlBtMzQyMkRCU0FpRUFoajFzeFo5Nm9ySVRzUXh5TUxJRFJKaW1wb3kxSjFNeWZsY1FWd2tremhzPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4eFowRjNTVUpCWjBsVlR6TjBiRlppVEhSMlRGQndLelo2UjA5MFpYQXhVMUJyVW1sbmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVhsTlZHdDRUMVJCTlZkb1kwNU5hbEYzVGtSQmVVMVVhM2xQVkVFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZPWkhKbWNHZE9WVEZTYW0xNksybzJOWEp3U2xkTFl6QTRjblZMV1hrMFJsZzNibTBLYlU5dVltRjFSbHBwYlhOUldISmtlVVJUV0V0T1VuUkZXRmcwV0ROMEwwRnRkQ3RsZFhkUVJFSm9LMlZ4TjBKRGJuRlBRMEZZYTNkblowWXhUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIVW14Q0NtaEVNSGQyZWtGbVRHSXlaRTFYVDJkUWNuSktkVkpyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwWjFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpoQ1NHOUJaVUZDTWtGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFjVUpCVVZvMFFVRkJVVVJCUldOM1VsRkpaMlZYVlcxMGJrUXdUVVpWYkRWcmExZzNibUpOWkV4WFEzTkVSMGxRZW1SSmJFNHJWMkZhUmpCVWJXdERDa2xSUXpjck16RnpZWEZ5Um1VNVVtMWtkVlphTW1SNFdHaFZVSEpoYW14MGRWTkVTR0l4ZGxOSFQyTjFTR3BCUzBKblozRm9hMnBQVUZGUlJFRjNUbThLUVVSQ2JFRnFSVUZ1TWl0MWRVeEljMjVJT1VSaU4zcHJTV1JHTmpWWmFHbFlZbWROVFVZdkwybElZeXRDTDFGRlZFc3dTRmxXWTA5UVZFc3pjRFEyUmdwVmVsaEdSRFo0Y2tGcVFVOHlhSEpyWm1wQ1MwRk9TMnBLU25oSVZqTkdWbkowVXl0VVVqQkhRMUF3U0hwRE0wUTNRbkk1TlZSWWVtWlBOeXRxTkVSa0NqZ3ZUaTloUVhJMlNXSnpQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "Xmrp3ljBF3vqYVtPy/sv1ojoxLUcLla2o46BeDPsJKI=" }, "signature": "MEUCIDQSJi9aertQhmT+ce1jKNdCe4KScsKGq9fPm3422DBSAiEAhj1sxZ96orITsQxyMLIDRJimpoy1J1MyflcQVwkkzhs=" } } ================================================ FILE: test/assets/integration/c.txt ================================================ DO NOT MODIFY ME! this is "c.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/offline-rekor.txt ================================================ DO NOT MODIFY ME! this is "offline-rekor.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/offline-rekor.txt.crt ================================================ -----BEGIN CERTIFICATE----- MIIEfzCCBAWgAwIBAgIULV3qds9Z1Ar1hOpW+/9ULyl1LgwwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjIxMDEzMTk0ODMyWhcNMjIxMDEzMTk1ODMyWjAAMHYwEAYH KoZIzj0CAQYFK4EEACIDYgAEff7HebXAkCGKe8/QMmJ3OCjSOhsR+3NGYn1FKm7R 672BvHek5Zza2D5bFDEwBEtM3E9hM2//OwN2EU8dK6BAaVGtlEHZvAzCcWCUwWFj 8QTp9eQDt3Hrmygyp9qB6mOro4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTxc4F9+1z0h4kG410C/f0NxerAhzAf BgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3 aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo dWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb fBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYPS5C1VAAAEAQIAO9w0 5StsDZsK27vfjH1nmzhB8dAcifwsCduL7XS079Jz9hUfcjqKMZOQbL5dlulkteqm oQPO272u/AxLca7gKDD47gBx0/O9yk6TapGQuqsNrn2JPpfMdvzwJvXQJ/7rL61l d6zs/3q0UQQu4PqVIdDPhNF9chUMGiau5UKACsManYKtmTi86+wcCT89Etb9SqSj +QiTlTzQqIi9cKXbUhOTzpiKALjwNvsvB5pQ6U9WN+8OVoQPr919js+O0AeVf8R6 YKhVumMBquvV756FocC/lxThYITbmUH91bY/nQPYy4tAhuums6Cc+9vzYaeQw6y0 dUfum1XM8agJsihYzuaL/U0S2n8HrfsLjLU6a06IPMEx7WVGSEZxTH78PurXDKB8 sLKG2X2wIQpiyglk6CU0zgw4WXb+qON7VFIL4wOe5tdrSHwRdV6xqGOdeSf+TyH4 7GRPa0raT2pVWAZf6liJPD4vqH2jJWE3WbhOWkfYM9uqoE1fQSQr7GN4+NJzmsdN scxsD2tiExlXNIMIvpXqTrbWSxDC/reMPjnbpNUHBCwqSyaL7HyW0oB3e6JJOuWl yFDJIimX2tpLWyMV4tLCMd/p3EZsE5oCs1cGOiDQhAVUTwJOtxH6jk+vhFDJSH6C gkyIyu8vQAwVGatCdElYKK6R0kt8/yA9szrsFMwwCgYIKoZIzj0EAwMDaAAwZQIx AJDWJS41EwSk8LLZyqBjK2rG77+ceBjD2Vx6h1oGHVGVBwsiq4CgPsEyPJtVW+1Q 8wIwZ/gMuXAzIllTHJ4HBFTkODEPUcVYctRDkF75V2lvtS4eO0JFc+agbn/Ah99V aprh -----END CERTIFICATE----- ================================================ FILE: test/assets/offline-rekor.txt.sig ================================================ MGUCMQCkHC+iuvTo9H1E4ygqCvSq+dAxbqO9Grg12GJDlRe0hMO+TdE/cn2KRB7VGonN0EMCMBvtIkjcIcbBSV0H8pPmpsZiH/OxWc5J7jyEJLERq/M71GamZOor9xx5x83L8Dg2HA== ================================================ FILE: test/assets/signing_config/signingconfig-only-v1-rekor.v2.json ================================================ { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.example.com", "majorApiVersion": 1, "validFor": { "start": "2023-04-14T21:38:40Z" }, "operator": "example.com" }, { "url": "https://fulcio-old.example.com", "majorApiVersion": 1, "validFor": { "start": "2022-04-14T21:38:40Z", "end": "2023-04-14T21:38:40Z" }, "operator": "example.com" } ], "oidcUrls": [ { "url": "https://oauth2.example.com/auth", "majorApiVersion": 1, "validFor": { "start": "2025-04-16T00:00:00Z" }, "operator": "example.com" } ], "rekorTlogUrls": [ { "url": "https://rekor.example.com", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27Z" }, "operator": "example.com" } ], "tsaUrls": [ { "url": "https://timestamp.example.com/api/v1/timestamp", "majorApiVersion": 1, "validFor": { "start": "2025-04-09T00:00:00Z" }, "operator": "example.com" } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } ================================================ FILE: test/assets/signing_config/signingconfig.v2.json ================================================ { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.example.com", "majorApiVersion": 1, "validFor": { "start": "2023-04-14T21:38:40Z" }, "operator": "example.com" }, { "url": "https://fulcio-old.example.com", "majorApiVersion": 1, "validFor": { "start": "2022-04-14T21:38:40Z", "end": "2023-04-14T21:38:40Z" }, "operator": "example.com" } ], "oidcUrls": [ { "url": "https://oauth2.example.com/auth", "majorApiVersion": 1, "validFor": { "start": "2025-04-16T00:00:00Z" }, "operator": "example.com" } ], "rekorTlogUrls": [ { "url": "https://rekor.example.com", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27Z" }, "operator": "example.com" }, { "url": "https://rekor-v2.example.com", "majorApiVersion": 2, "validFor": { "start": "2021-01-12T11:53:27Z" }, "operator": "example.com" } ], "tsaUrls": [ { "url": "https://timestamp.example.com/api/v1/timestamp", "majorApiVersion": 1, "validFor": { "start": "2025-04-09T00:00:00Z" }, "operator": "example.com" } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } ================================================ FILE: test/assets/staging-rekor-v2.txt ================================================ DO NOT MODIFY ME! this is "staging-rekor-v2.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/staging-rekor-v2.txt.sigstore.json ================================================ {"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": {"certificate": {"rawBytes": "MIICyzCCAlCgAwIBAgIUJc/6ox+xb+Cmb5UVrFhdu5jiMzIwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNjA5MTE1NzM1WhcNMjUwNjA5MTIwNzM1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvoYb1h6sjlOR276rCjnPc/PgZtTahLzmf32f08PZ/2eWr4q979itVw1PG8IhcK3E2ZiihegXEgh4mPkkMn78BKOCAW8wggFrMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUsoZlvpIKgR6WlgezvkD6xzHypcMwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwGQYDVR0RAQH/BA8wDYELamt1QGdvdG8uZmkwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGXVI2aFgAABAMARzBFAiBDHRpKGTpiU3Nx28XgewlvzbMt/ug6ipN8Xj9tryWbwQIhAP/3Cngo4St1nAggkflowySL0fPYg/QDcJKE6XceON3WMAoGCCqGSM49BAMDA2kAMGYCMQCfyQmcNbg2g5PD9Jrb9yOS+vEwwThoY2YDoptDzhJvOxNYLek6DRwCAjZ4SqeTwmQCMQDD3lXotLGsn/CJxGlEiVaF2+z3SKb+bLGGKQATHPkZ/XHvLI2cAdVhcTYeEn36shE="}, "tlogEntries": [{"logIndex": "645", "logId": {"keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.2"}, "inclusionProof": {"logIndex": "645", "rootHash": "kNum4JmdViJPfZLMRB3xPi6flATj2JzJSiF+1pQDzNQ=", "treeSize": "646", "hashes": ["eTqr8nE8VGEREKQ2MDQeD+zKHTJERE6iNw0tG1G+WbQ=", "wzbEsO0X3AWHadlgJZx7yhJdRVEZ2dEY21okXQ6UIi4=", "QMesRTEZdIgthOEinYE/9J7wGv+VmArDZTICj9POmhY=", "UNUMG62rMwoqCqFKknh4R5Ubkf5Z6dj+Pk0m/1xu8uo="], "checkpoint": {"envelope": "log2025-alpha1.rekor.sigstage.dev\n646\nkNum4JmdViJPfZLMRB3xPi6flATj2JzJSiF+1pQDzNQ=\n\n\u2014 log2025-alpha1.rekor.sigstage.dev 8w1amQA0XB55lIjvC/rvbpawQn9lp2R5TSkvqoNJuxcH9Ii05Ddi66xN8z5ZE6GsK2MkvgNZuqnZ5RtHbq2kpt/B8AE=\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJoYXNoZWRSZWtvcmRWMDAyIjp7ImRhdGEiOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiJGZlp5UmhGWklidDhIZURuNmVrblhJQVczQ1ZLREFDWWlKUkxmdE5rU3FvPSJ9LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQVo2VDhBVVpTQ0JaYUtKa3NMbFNpbE5xRUVPdDRaeUdNR2VwVXBLcDdWR0FpRUFzL1gwa01KVG5FT3V6L0RMV3hUTDR3QlZOa2lXSVVERjM2RUVENzAzOTZBPSIsInZlcmlmaWVyIjp7ImtleURldGFpbHMiOiJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsIng1MDlDZXJ0aWZpY2F0ZSI6eyJyYXdCeXRlcyI6Ik1JSUN5ekNDQWxDZ0F3SUJBZ0lVSmMvNm94K3hiK0NtYjVVVnJGaGR1NWppTXpJd0NnWUlLb1pJemowRUF3TXdOekVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SNHdIQVlEVlFRREV4VnphV2R6ZEc5eVpTMXBiblJsY20xbFpHbGhkR1V3SGhjTk1qVXdOakE1TVRFMU56TTFXaGNOTWpVd05qQTVNVEl3TnpNMVdqQUFNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUV2b1liMWg2c2psT1IyNzZyQ2puUGMvUGdadFRhaEx6bWYzMmYwOFBaLzJlV3I0cTk3OWl0VncxUEc4SWhjSzNFMlppaWhlZ1hFZ2g0bVBra01uNzhCS09DQVc4d2dnRnJNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBekFkQmdOVkhRNEVGZ1FVc29abHZwSUtnUjZXbGdlenZrRDZ4ekh5cGNNd0h3WURWUjBqQkJnd0ZvQVVjWVl3cGhSOFltLzU5OWIwQlJwL1gvL3JiNnd3R1FZRFZSMFJBUUgvQkE4d0RZRUxhbXQxUUdkdmRHOHVabWt3TEFZS0t3WUJCQUdEdnpBQkFRUWVhSFIwY0hNNkx5OW5hWFJvZFdJdVkyOXRMMnh2WjJsdUwyOWhkWFJvTUM0R0Npc0dBUVFCZzc4d0FRZ0VJQXdlYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDJ4dloybHVMMjloZFhSb01JR0tCZ29yQmdFRUFkWjVBZ1FDQkh3RWVnQjRBSFlBS3pDODNHaUl5ZUxoMkNZcFhuUWZTRGt4bGdMeW5EUExYa05BL3JLc2hub0FBQUdYVkkyYUZnQUFCQU1BUnpCRkFpQkRIUnBLR1RwaVUzTngyOFhnZXdsdnpiTXQvdWc2aXBOOFhqOXRyeVdid1FJaEFQLzNDbmdvNFN0MW5BZ2drZmxvd3lTTDBmUFlnL1FEY0pLRTZYY2VPTjNXTUFvR0NDcUdTTTQ5QkFNREEya0FNR1lDTVFDZnlRbWNOYmcyZzVQRDlKcmI5eU9TK3ZFd3dUaG9ZMllEb3B0RHpoSnZPeE5ZTGVrNkRSd0NBalo0U3FlVHdtUUNNUUREM2xYb3RMR3NuL0NKeEdsRWlWYUYyK3ozU0tiK2JMR0dLUUFUSFBrWi9YSHZMSTJjQWRWaGNUWWVFbjM2c2hFPSJ9fX19fX0="}], "timestampVerificationData": {"rfc3161Timestamps": [{"signedTimestamp": "MIIE6zADAgEAMIIE4gYJKoZIhvcNAQcCoIIE0zCCBM8CAQMxDTALBglghkgBZQMEAgEwgcMGCyqGSIb3DQEJEAEEoIGzBIGwMIGtAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgOjYPmS6Qixa9OQqdXWQMPN66194GUnV3liEVd7cbW8oCFQDuYcF6Hx3Wi2sgxpmG+IG2KlvUKRgPMjAyNTA2MDkxMTU3MzhaMAMCAQECCQCbf5cNt4JRDqAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgggITMIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6MYIB3DCCAdgCAQEwUTA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQKNaEGYdXiQXPGiZan8n3yfgN8pzALBglghkgBZQMEAgGggfwwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTA2MDkxMTU3MzhaMC8GCSqGSIb3DQEJBDEiBCA6qJ7IlNaN4uuHegN2O+NsWY5kB6sw8E/Q3H3arU8jmDCBjgYLKoZIhvcNAQkQAi8xfzB9MHsweQQgBvT/4Ef+s1mZtzOw16MjUBz8GOTAM2aoRdd1NudLJ0QwVTA9pDswOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwIEaDBmAjEA9vHFXY/Ia5L2g8F7ipZpiJOgDoAau7L+UkE5c1cCM2FYDZN1QQzWjXGj1CwQMOcuAjEAtBIxQiiecOzOkFo1Bj0n9xkIjyErSBT+P3P6OWgwdivDosxQCTMF7iNeI7wgFQxw"}]}}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "FfZyRhFZIbt8HeDn6eknXIAW3CVKDACYiJRLftNkSqo="}, "signature": "MEUCIAZ6T8AUZSCBZaKJksLlSilNqEEOt4ZyGMGepUpKp7VGAiEAs/X0kMJTnEOuz/DLWxTL4wBVNkiWIUDF36EED70396A="}} ================================================ FILE: test/assets/staging-tuf/16.snapshot.json ================================================ { "signatures": [ { "keyid": "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4", "sig": "304402202733036a5044a3257392cb6737c80d1972aa2bce8e7194fac23e3d0b939e83ce0220797111c4aa47094278a2997d727c728fcda795b02b8ec803e2265fdac9614a21" } ], "signed": { "_type": "snapshot", "expires": "2035-06-11T11:54:57Z", "meta": { "registry.npmjs.org.json": { "version": 5 }, "targets.json": { "version": 17 } }, "spec_version": "1.0", "version": 16 } } ================================================ FILE: test/assets/staging-tuf/17.targets.json ================================================ { "signatures": [ { "keyid": "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81", "sig": "3045022031cbae59944160c1b9b1df859c43cf74d8c5257c32924f1c78146ccd621aae53022100cc8097664966a0f187e41643a61524613434517ec97c9a21f319752fd842e122" }, { "keyid": "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc", "sig": "30440220149fb96582721bcaf506b06465cf8df9b4b4c7847f19165eec8f7faeccc61ed8022020090a30e448e7cd71824bf0042ce9982b8882e557be343a919ffc4d825927f6" }, { "keyid": "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237", "sig": "" }, { "keyid": "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5", "sig": "" } ], "signed": { "_type": "targets", "delegations": { "keys": { "5e3a4021b11a425fd0a444f1670457ce5b15bbe036144f2417426f7f4b9721da": { "keytype": "ecdsa", "keyval": { "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVfei1dXQRVeArCMcTDgxJtYg+Fs7\nV87DjhQbGlRJPyC7SW5TbNNkmvpmi4LeTv6moLVZ7T2nVqiRZbSkD+cf8w==\n-----END PUBLIC KEY-----\n" }, "scheme": "ecdsa-sha2-nistp256", "x-tuf-on-ci-online-uri": "azurekms://npm-tuf-delegate.vault.azure.net/keys/npm-tuf-delegate-2024-08/e2772c1d01ca400da571096889f1660e" } }, "roles": [ { "keyids": [ "5e3a4021b11a425fd0a444f1670457ce5b15bbe036144f2417426f7f4b9721da" ], "name": "registry.npmjs.org", "paths": [ "registry.npmjs.org/*" ], "terminating": true, "threshold": 1 } ] }, "expires": "2035-06-10T18:17:38Z", "spec_version": "1.0", "targets": { "ctfe.pub": { "custom": { "sigstore": { "status": "Active", "uri": "https://ctfe.sigstage.dev/test", "usage": "CTFE" } }, "hashes": { "sha256": "bd7a6812a1f239dfddbbb19d36c7423d21510da56d466ba5018401959cd66037" }, "length": 775 }, "ctfe_2022.pub": { "custom": { "sigstore": { "status": "Active", "uri": "https://ctfe.sigstage.dev/2022", "usage": "CTFE" } }, "hashes": { "sha256": "910d899c7763563095a0fe684c8477573fedc19a78586de6ecfbfd8f289f5423" }, "length": 178 }, "ctfe_2022_2.pub": { "custom": { "sigstore": { "status": "Active", "uri": "https://ctfe.sigstage.dev/2022-2", "usage": "CTFE" } }, "hashes": { "sha256": "7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6" }, "length": 178 }, "fulcio.crt.pem": { "custom": { "sigstore": { "status": "Active", "uri": "https://fulcio.sigstage.dev", "usage": "Fulcio" } }, "hashes": { "sha256": "0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1" }, "length": 741 }, "fulcio_intermediate.crt.pem": { "custom": { "sigstore": { "status": "Active", "uri": "https://fulcio.sigstage.dev", "usage": "Fulcio" } }, "hashes": { "sha256": "782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b" }, "length": 790 }, "rekor.pub": { "custom": { "sigstore": { "status": "Active", "uri": "https://rekor.sigstage.dev", "usage": "Rekor" } }, "hashes": { "sha256": "1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959" }, "length": 178 }, "signing_config.json": { "hashes": { "sha256": "bf52f4aa7dc05849a6c8c760f5ae2ea4047b03b59505d9280efe02a1ec63c6e8" }, "length": 220 }, "signing_config.v0.2.json": { "hashes": { "sha256": "0f395087486ba318321eda478d847962b1dd89846c7dc6e95752a6b110669393" }, "length": 1022 }, "trusted_root.json": { "hashes": { "sha256": "ed6a9cf4e7c2e3297a4b5974fce0d17132f03c63512029d7aa3a402b43acab49" }, "length": 6824 } }, "version": 17, "x-tuf-on-ci-expiry-period": 3650, "x-tuf-on-ci-signing-period": 365 } } ================================================ FILE: test/assets/staging-tuf/targets/0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1.fulcio.crt.pem ================================================ -----BEGIN CERTIFICATE----- MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAq MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy MDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUu ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9 BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUEC CWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNj MGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9C Mrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6H j2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm 45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTr y3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw= -----END CERTIFICATE----- ================================================ FILE: test/assets/staging-tuf/targets/0f395087486ba318321eda478d847962b1dd89846c7dc6e95752a6b110669393.signing_config.v0.2.json ================================================ { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.sigstage.dev", "majorApiVersion": 1, "validFor": { "start": "2022-04-14T21:38:40Z" }, "operator": "sigstore.dev" } ], "oidcUrls": [ { "url": "https://oauth2.sigstage.dev/auth", "majorApiVersion": 1, "validFor": { "start": "2025-04-16T00:00:00Z" }, "operator": "sigstore.dev" } ], "rekorTlogUrls": [ { "url": "https://rekor.sigstage.dev", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27Z" }, "operator": "sigstore.dev" } ], "tsaUrls": [ { "url": "https://timestamp.sigstage.dev/api/v1/timestamp", "majorApiVersion": 1, "validFor": { "start": "2025-04-09T00:00:00Z" }, "operator": "sigstore.dev" } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } ================================================ FILE: test/assets/staging-tuf/targets/1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959.rekor.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9 nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg== -----END PUBLIC KEY----- ================================================ FILE: test/assets/staging-tuf/targets/7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6.ctfe_2022_2.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHq c24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ== -----END PUBLIC KEY----- ================================================ FILE: test/assets/staging-tuf/targets/782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b.fulcio_intermediate.crt.pem ================================================ -----BEGIN CERTIFICATE----- MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAq MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy MDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUu ZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB BgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9Kt NfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWI JEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEF BQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQF Gn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjO PQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1O HHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/ KX1SBrKQu220FmVL0Q== -----END CERTIFICATE----- ================================================ FILE: test/assets/staging-tuf/targets/ed6a9cf4e7c2e3297a4b5974fce0d17132f03c63512029d7aa3a402b43acab49.trusted_root.json ================================================ { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "https://rekor.sigstage.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27Z" } }, "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" } }, { "baseUrl": "https://log2025-alpha1.rekor.sigstage.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MCowBQYDK2VwAyEAPn+AREHoBaZ7wgS1zBqpxmLSGnyhxXj4lFxSdWVB8o8=", "keyDetails": "PKIX_ED25519", "validFor": { "start": "2025-04-16T00:00:00Z" } }, "logId": { "keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstage.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" }, { "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" } ] }, "validFor": { "start": "2022-04-14T21:38:40Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstage.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", "keyDetails": "PKCS1_RSA_PKCS1V5", "validFor": { "start": "2021-03-14T00:00:00Z", "end": "2022-07-31T00:00:00Z" } }, "logId": { "keyId": "G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=" } }, { "baseUrl": "https://ctfe.sigstage.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-07-01T00:00:00Z", "end": "2022-07-31T00:00:00Z" } }, "logId": { "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" } }, { "baseUrl": "https://ctfe.sigstage.dev/2022-2", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-07-01T00:00:00Z" } }, "logId": { "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" } } ], "timestampAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore-tsa-selfsigned" }, "uri": "https://timestamp.sigstage.dev/api/v1/timestamp", "certChain": { "certificates": [ { "rawBytes": "MIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUCPExEFKiQh0dP4sp5ltmSYSSkFUwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATt0tIDWyo4ARfL9BaSo0W5bJQEbKJTU/u7llvdjSI5aTkOAJa8tixn2+LEfPG4dMFdsMPtsIuU1qn2OqFiuMk6vHv/c+az25RQVY1oo50iMb0jIL3N4FgwhPFpZnCbQPOjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAKBggqhkjOPQQDAwNpADBmAjEA2MI1VXgbf3dUOSc95hSRypBKOab18eh2xzQtxUsHvWeY+1iFgyMluUuNR6taoSmFAjEA31m2czguZhKYX+4JSKu5pRYhBTXAd8KKQ3xdPRX/qCaLvT2qJAEQ1YQM3EJRrtI7" } ] }, "validFor": { "start": "2025-04-09T00:00:00Z" } } ] } ================================================ FILE: test/assets/staging-tuf/timestamp.json ================================================ { "signatures": [ { "keyid": "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4", "sig": "3046022100fedb5a3d1a3c461c1337d7535edca8012fb0ab8da31315dbdf22b7f38f76973e022100a87967789d2d2942919dcc4f33def8ee74745f577ff0ef5479cc9f573842e8de" } ], "signed": { "_type": "timestamp", "expires": "2025-07-29T13:28:44Z", "meta": { "snapshot.json": { "version": 16 } }, "spec_version": "1.0", "version": 353 } } ================================================ FILE: test/assets/trust_config/config.badtype.json ================================================ { "mediaType": "bad-media-type", "trustedRoot": { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "https://rekor.sigstore.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27.000Z" } }, "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" } ] }, "validFor": { "start": "2021-03-07T03:20:29.000Z", "end": "2022-12-31T23:59:59.999Z" } }, { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" } ] }, "validFor": { "start": "2022-04-13T20:06:15.000Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstore.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-03-14T00:00:00.000Z", "end": "2022-10-31T23:59:59.999Z" } }, "logId": { "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" } }, { "baseUrl": "https://ctfe.sigstore.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-10-20T00:00:00.000Z" } }, "logId": { "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" } } ], "timestampAuthorities": [ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [ { "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" }, { "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" }, { "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" } ] }, "validFor": { "start": "2023-04-14T00:00:00.000Z" } } ] }, "signing_config": { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.example.com", "majorApiVersion": 1, "validFor": { "start": "2023-04-14T21:38:40Z" } }, { "url": "https://fulcio-old.example.com", "majorApiVersion": 1, "validFor": { "start": "2022-04-14T21:38:40Z", "end": "2023-04-14T21:38:40Z" } } ], "oidcUrls": [ { "url": "https://oauth2.example.com/auth", "majorApiVersion": 1, "validFor": { "start": "2025-04-16T00:00:00Z" } } ], "rekorTlogUrls": [ { "url": "https://rekor.example.com", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27Z" } }, { "url": "https://rekor-v2.example.com", "majorApiVersion": 2, "validFor": { "start": "2021-01-12T11:53:27Z" } } ], "tsaUrls": [ { "url": "https://timestamp.example.com/api/v1/timestamp", "majorApiVersion": 1, "validFor": { "start": "2025-04-09T00:00:00Z" } } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } } ================================================ FILE: test/assets/trust_config/config.v1.json ================================================ { "mediaType": "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json", "trustedRoot": { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "https://rekor.sigstore.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27.000Z" } }, "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" } ] }, "validFor": { "start": "2021-03-07T03:20:29.000Z", "end": "2022-12-31T23:59:59.999Z" } }, { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" } ] }, "validFor": { "start": "2022-04-13T20:06:15.000Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstore.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-03-14T00:00:00.000Z", "end": "2022-10-31T23:59:59.999Z" } }, "logId": { "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" } }, { "baseUrl": "https://ctfe.sigstore.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-10-20T00:00:00.000Z" } }, "logId": { "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" } } ], "timestampAuthorities": [ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [ { "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" }, { "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" }, { "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" } ] }, "validFor": { "start": "2023-04-14T00:00:00.000Z" } } ] }, "signing_config": { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.example.com", "majorApiVersion": 1, "validFor": { "start": "2023-04-14T21:38:40Z" }, "operator": "example.com" }, { "url": "https://fulcio-old.example.com", "majorApiVersion": 1, "validFor": { "start": "2022-04-14T21:38:40Z", "end": "2023-04-14T21:38:40Z" }, "operator": "example.com" } ], "oidcUrls": [ { "url": "https://oauth2.example.com/auth", "majorApiVersion": 1, "validFor": { "start": "2025-04-16T00:00:00Z" }, "operator": "example.com" } ], "rekorTlogUrls": [ { "url": "https://rekor.example.com", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27Z" }, "operator": "example.com" }, { "url": "https://rekor-v2.example.com", "majorApiVersion": 2, "validFor": { "start": "2021-01-12T11:53:27Z" }, "operator": "example.com" } ], "tsaUrls": [ { "url": "https://timestamp.example.com/api/v1/timestamp", "majorApiVersion": 1, "validFor": { "start": "2025-04-09T00:00:00Z" }, "operator": "example.com" } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } } ================================================ FILE: test/assets/trusted_root/certificate_authority.empty.json ================================================ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [] }, "validFor": { "start": "2023-04-14T00:00:00.000Z", "end": "2024-04-14T00:00:00.000Z" } } ================================================ FILE: test/assets/trusted_root/certificate_authority.json ================================================ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [ { "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" }, { "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" }, { "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" } ] }, "validFor": { "start": "2023-04-14T00:00:00.000Z", "end": "2024-04-14T00:00:00.000Z" } } ================================================ FILE: test/assets/trusted_root/certificate_authority.missingroot.json ================================================ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [ { "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" }, { "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" } ] }, "validFor": { "start": "2023-04-14T00:00:00.000Z", "end": "2024-04-14T00:00:00.000Z" } } ================================================ FILE: test/assets/trusted_root/trustedroot.badtype.json ================================================ { "mediaType": "bad-media-type", "tlogs": [ { "baseUrl": "https://rekor.sigstore.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27.000Z" } }, "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" } ] }, "validFor": { "start": "2021-03-07T03:20:29.000Z", "end": "2022-12-31T23:59:59.999Z" } }, { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" } ] }, "validFor": { "start": "2022-04-13T20:06:15.000Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstore.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-03-14T00:00:00.000Z", "end": "2022-10-31T23:59:59.999Z" } }, "logId": { "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" } }, { "baseUrl": "https://ctfe.sigstore.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-10-20T00:00:00.000Z" } }, "logId": { "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" } } ], "timestampAuthorities": [ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [ { "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" }, { "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" }, { "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" } ] }, "validFor": { "start": "2023-04-14T00:00:00.000Z" } } ] } ================================================ FILE: test/assets/trusted_root/trustedroot.v1.json ================================================ { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "https://rekor.sigstore.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27.000Z" } }, "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" } ] }, "validFor": { "start": "2021-03-07T03:20:29.000Z", "end": "2022-12-31T23:59:59.999Z" } }, { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" } ] }, "validFor": { "start": "2022-04-13T20:06:15.000Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstore.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-03-14T00:00:00.000Z", "end": "2022-10-31T23:59:59.999Z" } }, "logId": { "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" } }, { "baseUrl": "https://ctfe.sigstore.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-10-20T00:00:00.000Z" } }, "logId": { "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" } } ], "timestampAuthorities": [ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [ { "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" }, { "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" }, { "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" } ] }, "validFor": { "start": "2023-04-14T00:00:00.000Z" } } ] } ================================================ FILE: test/assets/trusted_root/trustedroot.v1.local_tlog_ed25519_rekor-tiles.json ================================================ { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "http://localhost:3003", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MCowBQYDK2VwAyEAREvJyNZGjX6B3DAIuD3BTg9rIwV00GY8Xg5FU+IFDUQ=", "keyDetails": "PKIX_ED25519", "validFor": { "start": "1970-01-01T00:00:00Z" } }, "logId": { "keyId": "tAlACZWkUrif9Z9sOIrpk1ak1I8loRNufk79N6l1SNg=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" } ] }, "validFor": { "start": "2021-03-07T03:20:29.000Z", "end": "2022-12-31T23:59:59.999Z" } }, { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstore.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" }, { "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" } ] }, "validFor": { "start": "2022-04-13T20:06:15.000Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstore.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-03-14T00:00:00.000Z", "end": "2022-10-31T23:59:59.999Z" } }, "logId": { "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" } }, { "baseUrl": "https://ctfe.sigstore.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-10-20T00:00:00.000Z" } }, "logId": { "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" } } ], "timestampAuthorities": [ { "subject": { "organization": "GitHub, Inc.", "commonName": "Internal Services Root" }, "certChain": { "certificates": [ { "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" }, { "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" }, { "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" } ] }, "validFor": { "start": "2023-04-14T00:00:00.000Z" } } ] } ================================================ FILE: test/assets/tsa/bundle.duplicate.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { "rawBytes": "MIIC2TCCAl6gAwIBAgIUdmztZIKhChYc16oLF65pX34wgpowCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAyMzM5WhcNMjQxMDMxMTAzMzM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6jFpMi07y77fdwwYmgZ8mMsiORhq9OYO/1KtrJJFHl1yrnN6hpX7vC5affuipObcL3utSgCAnwN1QCAfumx5VqOCAX0wggF5MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUaMSROcZrZvwW7N6tp6yjzkI5QmkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGS4hotJAAABAMARjBEAiB3YxcguZbssCo28dz3BTlBf2RNwL3GOicOIecLahdeJgIgA0RNy/ARrGW2iAnM1PWT/gBgHcQ+wk0hD4FFAmM5JrYwCgYIKoZIzj0EAwMDaQAwZgIxANwxTWEcb9oFkCo63tNd8/ueYAKpsowGyyQs+AX0CE0XJiHjc24HT57G9CP3XYRCnwIxAITQtm0+VvPufhJGvMtn6K0okqWWZFFJQrz0akRlBHHk3osCdhENY0ZBmT8f+59b7Q==" }, "tlogEntries": [ { "logIndex": "35355462", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1730370219", "inclusionPromise": { "signedEntryTimestamp": "MEQCIFWlAKfTUTVLdRAkICb7QjK9wWa5clIPSO/I2as7NemMAiAptKOQSwFZsdM/T36yjDhXu4i4i32iy4mLDKFH2SBmAw==" }, "inclusionProof": { "logIndex": "3673050", "rootHash": "CRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=", "treeSize": "3673051", "hashes": [ "PaodjVERCZrJ4m+Ux1vKwci70JNV1o7i6tg+r7emiLU=", "hb5Kc++ml8xcjeNY59TfzSSnPGhTQqnl+7VhO4Vr6a8=", "pVIutklD+cs4kcBFMp3iPbw/Kn/rWtdwTHwh87zm/so=", "eUTldsq4LV/OSczlwUFHxK6yY1+kE/ASoidYXY1zybw=", "2rA1/K1G+of0n4dAsYaj4AlV4MWHM7CJz24RmIrEfhs=", "P8eXf78ohkRkntQNFfarUtn9Gct7yy+smjM5cersyUg=", "3Ul1Loa16XnnGTifeAYy8nlO0JyNIL6E/ZWE1tuIE9w=", "mU9v3N0cr/U/8VEM8R56E8z5ScHbeALqtChTUlAmTr4=", "70FF4PlelNUMSWeGPKROonP6S+1hpHMe5r5uwLPhuro=", "ZS9WKtLvUQYFzFNmaQP+2Gtstl9yM3150pk+oqIMMHU=", "lRbgwAuY5l5kOuRQN6uQ8zRQJ5ntgvHUCcNOBOI4Wyg=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8202293616175992157\n3673051\nCRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\n\n— rekor.sigstage.dev 0y8wozBFAiAwPJa5KEL421/AQF8uo81cctm4t9lIY6IGmeH2fV9d1QIhAM6j+/flHM4dEyf5sKCNwyKt9nb9DBLlTHDsPOIrTkyQ\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGFrYXhGYTd2WkFHV01LMWV1dWMyNkxYY3p0VHJEeUkyT1NmN1lGNXFFNkFpQWkvVTNVbzR6R0RuKytaZTlpUjJEcHMzbElTRXpDTkNmZUJyc0VtMVhHaUE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXlWRU5EUVd3MlowRjNTVUpCWjBsVlpHMTZkRnBKUzJoRGFGbGpNVFp2VEVZMk5YQllNelIzWjNCdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFFVFhoTlZFRjVUWHBOTlZkb1kwNU5hbEY0VFVSTmVFMVVRWHBOZWswMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUyYWtad1RXa3dOM2szTjJaa2QzZFpiV2RhT0cxTmMybFBVbWh4T1U5WlR5OHhTM1FLY2twS1JraHNNWGx5Yms0MmFIQllOM1pETldGbVpuVnBjRTlpWTB3emRYUlRaME5CYm5kT01WRkRRV1oxYlhnMVZuRlBRMEZZTUhkblowWTFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZoVFZOU0NrOWpXbkphZG5kWE4wNDJkSEEyZVdwNmEwazFVVzFyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDB4bldVUldVakJTUVZGSUwwSkRVWGRKYjBWbldWZDRiR1ZIYkhwTWJVNXZXVmQ0YzFsWE5XdGFWVUl3WTIxR2NHSkhPVzFaYld3d1kzazFhZ3BpTWpCM1MxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVXSmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5RM05IQ2tOcGMwZEJVVkZDWnpjNGQwRlJaMFZJVVhkaVlVaFNNR05JVFRaTWVUbG9XVEpPZG1SWE5UQmplVFZ1WWpJNWJtSkhWWFZaTWpsMFRVbEhTa0puYjNJS1FtZEZSVUZrV2pWQloxRkRRa2h6UldWUlFqTkJTRlZCUzNwRE9ETkhhVWw1WlV4b01rTlpjRmh1VVdaVFJHdDRiR2RNZVc1RVVFeFlhMDVCTDNKTGN3cG9ibTlCUVVGSFV6Um9iM1JLUVVGQlFrRk5RVkpxUWtWQmFVSXpXWGhqWjNWYVluTnpRMjh5T0dSNk0wSlViRUptTWxKT2Qwd3pSMDlwWTA5SlpXTk1DbUZvWkdWS1owbG5RVEJTVG5rdlFWSnlSMWN5YVVGdVRURlFWMVF2WjBKblNHTlJLM2RyTUdoRU5FWkdRVzFOTlVweVdYZERaMWxKUzI5YVNYcHFNRVVLUVhkTlJHRlJRWGRhWjBsNFFVNTNlRlJYUldOaU9XOUdhME52TmpOMFRtUTRMM1ZsV1VGTGNITnZkMGQ1ZVZGekswRllNRU5GTUZoS2FVaHFZekkwU0FwVU5UZEhPVU5RTTFoWlVrTnVkMGw0UVVsVVVYUnRNQ3RXZGxCMVptaEtSM1pOZEc0MlN6QnZhM0ZYVjFwR1JrcFJjbm93WVd0U2JFSklTR3N6YjNORENtUm9SVTVaTUZwQ2JWUTRaaXMxT1dJM1VUMDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==" } ], "timestampVerificationData": { "rfc3161Timestamps": [ { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUZgvCEikoheDobrNm4nFYRaN++jkYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBI8WEfV8xmrlvkaOvelfoYFq1oJACKgeSBC5v4D4Ht4zCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmyRR9E8xnTxNsHflN8+FaAe8AC0s/iArTyOU11g8tnwCIAhCfSMG58DirT9dvDE4qS+lf2u+4c5Zcj8acL/pABxC" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJuBUaVBL+WdW9SX22H1VG/z6MgQYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCADIhONgTZhBhqyN4MtyNBn9si5kmswFNzntVyq29yKSjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlx7HrL52iXvlxB2EbVdH0YBmw9pom2useI+HOoJV3WQCIA2W22DynN8rtB+Rb947RvYmrV8co9tXhU0lRnfoNriU" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUZgvCEikoheDobrNm4nFYRaN++jkYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBI8WEfV8xmrlvkaOvelfoYFq1oJACKgeSBC5v4D4Ht4zCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmyRR9E8xnTxNsHflN8+FaAe8AC0s/iArTyOU11g8tnwCIAhCfSMG58DirT9dvDE4qS+lf2u+4c5Zcj8acL/pABxC" } ] } }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=" }, "signature": "MEQCIHakaxFa7vZAGWMK1euuc26LXcztTrDyI2OSf7YF5qE6AiAi/U3Uo4zGDn++Ze9iR2Dps3lISEzCNCfeBrsEm1XGiA==" } } ================================================ FILE: test/assets/tsa/bundle.many_timestamp.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { "rawBytes": "MIIC2TCCAl6gAwIBAgIUdmztZIKhChYc16oLF65pX34wgpowCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAyMzM5WhcNMjQxMDMxMTAzMzM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6jFpMi07y77fdwwYmgZ8mMsiORhq9OYO/1KtrJJFHl1yrnN6hpX7vC5affuipObcL3utSgCAnwN1QCAfumx5VqOCAX0wggF5MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUaMSROcZrZvwW7N6tp6yjzkI5QmkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGS4hotJAAABAMARjBEAiB3YxcguZbssCo28dz3BTlBf2RNwL3GOicOIecLahdeJgIgA0RNy/ARrGW2iAnM1PWT/gBgHcQ+wk0hD4FFAmM5JrYwCgYIKoZIzj0EAwMDaQAwZgIxANwxTWEcb9oFkCo63tNd8/ueYAKpsowGyyQs+AX0CE0XJiHjc24HT57G9CP3XYRCnwIxAITQtm0+VvPufhJGvMtn6K0okqWWZFFJQrz0akRlBHHk3osCdhENY0ZBmT8f+59b7Q==" }, "tlogEntries": [ { "logIndex": "35355462", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1730370219", "inclusionPromise": { "signedEntryTimestamp": "MEQCIFWlAKfTUTVLdRAkICb7QjK9wWa5clIPSO/I2as7NemMAiAptKOQSwFZsdM/T36yjDhXu4i4i32iy4mLDKFH2SBmAw==" }, "inclusionProof": { "logIndex": "3673050", "rootHash": "CRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=", "treeSize": "3673051", "hashes": [ "PaodjVERCZrJ4m+Ux1vKwci70JNV1o7i6tg+r7emiLU=", "hb5Kc++ml8xcjeNY59TfzSSnPGhTQqnl+7VhO4Vr6a8=", "pVIutklD+cs4kcBFMp3iPbw/Kn/rWtdwTHwh87zm/so=", "eUTldsq4LV/OSczlwUFHxK6yY1+kE/ASoidYXY1zybw=", "2rA1/K1G+of0n4dAsYaj4AlV4MWHM7CJz24RmIrEfhs=", "P8eXf78ohkRkntQNFfarUtn9Gct7yy+smjM5cersyUg=", "3Ul1Loa16XnnGTifeAYy8nlO0JyNIL6E/ZWE1tuIE9w=", "mU9v3N0cr/U/8VEM8R56E8z5ScHbeALqtChTUlAmTr4=", "70FF4PlelNUMSWeGPKROonP6S+1hpHMe5r5uwLPhuro=", "ZS9WKtLvUQYFzFNmaQP+2Gtstl9yM3150pk+oqIMMHU=", "lRbgwAuY5l5kOuRQN6uQ8zRQJ5ntgvHUCcNOBOI4Wyg=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8202293616175992157\n3673051\nCRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\n\n— rekor.sigstage.dev 0y8wozBFAiAwPJa5KEL421/AQF8uo81cctm4t9lIY6IGmeH2fV9d1QIhAM6j+/flHM4dEyf5sKCNwyKt9nb9DBLlTHDsPOIrTkyQ\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGFrYXhGYTd2WkFHV01LMWV1dWMyNkxYY3p0VHJEeUkyT1NmN1lGNXFFNkFpQWkvVTNVbzR6R0RuKytaZTlpUjJEcHMzbElTRXpDTkNmZUJyc0VtMVhHaUE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXlWRU5EUVd3MlowRjNTVUpCWjBsVlpHMTZkRnBKUzJoRGFGbGpNVFp2VEVZMk5YQllNelIzWjNCdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFFVFhoTlZFRjVUWHBOTlZkb1kwNU5hbEY0VFVSTmVFMVVRWHBOZWswMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUyYWtad1RXa3dOM2szTjJaa2QzZFpiV2RhT0cxTmMybFBVbWh4T1U5WlR5OHhTM1FLY2twS1JraHNNWGx5Yms0MmFIQllOM1pETldGbVpuVnBjRTlpWTB3emRYUlRaME5CYm5kT01WRkRRV1oxYlhnMVZuRlBRMEZZTUhkblowWTFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZoVFZOU0NrOWpXbkphZG5kWE4wNDJkSEEyZVdwNmEwazFVVzFyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDB4bldVUldVakJTUVZGSUwwSkRVWGRKYjBWbldWZDRiR1ZIYkhwTWJVNXZXVmQ0YzFsWE5XdGFWVUl3WTIxR2NHSkhPVzFaYld3d1kzazFhZ3BpTWpCM1MxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVXSmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5RM05IQ2tOcGMwZEJVVkZDWnpjNGQwRlJaMFZJVVhkaVlVaFNNR05JVFRaTWVUbG9XVEpPZG1SWE5UQmplVFZ1WWpJNWJtSkhWWFZaTWpsMFRVbEhTa0puYjNJS1FtZEZSVUZrV2pWQloxRkRRa2h6UldWUlFqTkJTRlZCUzNwRE9ETkhhVWw1WlV4b01rTlpjRmh1VVdaVFJHdDRiR2RNZVc1RVVFeFlhMDVCTDNKTGN3cG9ibTlCUVVGSFV6Um9iM1JLUVVGQlFrRk5RVkpxUWtWQmFVSXpXWGhqWjNWYVluTnpRMjh5T0dSNk0wSlViRUptTWxKT2Qwd3pSMDlwWTA5SlpXTk1DbUZvWkdWS1owbG5RVEJTVG5rdlFWSnlSMWN5YVVGdVRURlFWMVF2WjBKblNHTlJLM2RyTUdoRU5FWkdRVzFOTlVweVdYZERaMWxKUzI5YVNYcHFNRVVLUVhkTlJHRlJRWGRhWjBsNFFVNTNlRlJYUldOaU9XOUdhME52TmpOMFRtUTRMM1ZsV1VGTGNITnZkMGQ1ZVZGekswRllNRU5GTUZoS2FVaHFZekkwU0FwVU5UZEhPVU5RTTFoWlVrTnVkMGw0UVVsVVVYUnRNQ3RXZGxCMVptaEtSM1pOZEc0MlN6QnZhM0ZYVjFwR1JrcFJjbm93WVd0U2JFSklTR3N6YjNORENtUm9SVTVaTUZwQ2JWUTRaaXMxT1dJM1VUMDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==" } ], "timestampVerificationData": { "rfc3161Timestamps": [ { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUZgvCEikoheDobrNm4nFYRaN++jkYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBI8WEfV8xmrlvkaOvelfoYFq1oJACKgeSBC5v4D4Ht4zCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmyRR9E8xnTxNsHflN8+FaAe8AC0s/iArTyOU11g8tnwCIAhCfSMG58DirT9dvDE4qS+lf2u+4c5Zcj8acL/pABxC" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJuBUaVBL+WdW9SX22H1VG/z6MgQYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCADIhONgTZhBhqyN4MtyNBn9si5kmswFNzntVyq29yKSjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlx7HrL52iXvlxB2EbVdH0YBmw9pom2useI+HOoJV3WQCIA2W22DynN8rtB+Rb947RvYmrV8co9tXhU0lRnfoNriU" }, { "signedTimestamp": "MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAInPJsUcb45j2wREk+YYo4TWAvEKGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgltd3hQPYZ4mepYN7yuTWP5rls2yho0g5Y3PJxye8ljswgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhAK6CcXH6ZS05vw0kPGz1d8XZ6E4mWBa/uCH6rTlgEG7QAiEA1GVR+SPQ4yaY4KpsuOOHzftnHFaFh1M+nFJ3UQqasEg=" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUIT28EHNSU/e7tTi0z06mlsLKhWcYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHeMIIB2gIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDJw9ILGbCv13QFU6uDNfRYJB7gQqaEJrvGc+KmLjX5MjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIESDBGAiEA8/+MZO19ZWXJQxSS8BuNRPv9kBtCEwKSppWPLwVGv3ICIQCxyttCk9ZiF4H9yqEDS1nM1kaZJ2GwVYNSlAB0UFlGyA==" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAKOZ8Vbs/XHOiYrwAu7rwCzfUynwGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQg8EBkMvj98bWZRrUFrIuu1ESqfgZz3bl30t70EcmMmGowgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgGB67fQDA0aQeCox67ZML9eysx+Hh5P7xMc6JqffOUc4CIQCoHzV3vC72XZ8uLdW/bJZmnDsydoMbAGZ6L+9+K2yeLQ==" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJK4QC2mcxSdWnCqd1bF0JQo/lgsYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDFl7LJGj2Ly6mrN3rzvyj+h0hRUK4/mvHEgTXCpy9K3DCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlNj866pok1LTFRuzxIfu+h/KJ/kHmKnUfNF4PL2cdgsCIEKRudmJVaifKu72aNwiMB+P1YicRzgl/QGQPNAYDxUe" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUf3SIz6Ht3dk2YgyjaHAaBZs/lKsYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCwUeUZs9/xTDTrX3wn8y4jIrJcg4x1gB0H6eduxRHBeDCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEA5KdiLgcy7vmULd6k+fPCsHKeUv3K/Zy9+S6Sy2tE/jQCICB/GiGCKVP8hXI12VMfoVZMndJAh6t9Zq63JqIqkygW" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAJHdw6fXDetoIFJW9Hhd9B3XjzwnGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgJD6UCRd9vvJ16BbydH4H6VG+7aQvNpRAfk47sO/AXBMwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgGb8MyozlUpYJuYHOFMM3GVMuz3ljepDKTa2c4KvLYQgCIQCWzPS4iv1RAgr45jW+aPt4pmLNvnr2R9rvRyLLeEqvEg==" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAMLU2ENJMM+YtgW6Ej17g4dY1BXzGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3DCCAdgCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgo6YdXzqPFS778KAUN8h8L8iBJ2PyMUVNVOXyt3qISu0wgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEYwRAIgMCSlSF5sZxIBJh2KJ3dMdsQNKsuFrBYwxE8BM/ByjM0CICrcfQgicy7Vb0cuaZWKWyB/+1uRJc274srHesI50wWT" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVALZi3hcEqREKdAllVyl2vVL2fYX2GA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3DCCAdgCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgJBXiPjyQL6u9Cfc93/ehFPceaShX4VjKbDHcMtLTa1QwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEYwRAIgUL6zosIZSbdCs/ABNWcHlTuv2S1gN4djN6cXG7BvHv0CIDvwYlb8wMjAZanyWRfotKtYTL7Ye05xxWw9e4fMe38e" }, { "signedTimestamp": "MIIEyTADAgEAMIIEwAYJKoZIhvcNAQcCoIIEsTCCBK0CAQMxDTALBglghkgBZQMEAgEwgeMGCyqGSIb3DQEJEAEEoIHTBIHQMIHNAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwITZ8VwQISPxP7v+HsNtCiRc9y60hgPMjAyNDEwMzExMzMxNDRaMAMCAQECCQDeEKL9dsIkd6A0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZ6CCAdAwggHMMIIBcqADAgECAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlMB4XDTI0MTAzMTEwMTY0MloXDTMzMTAzMTEwMTk0MlowMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFsBczJAcgy5m1djVteUhYIoM8q2zWPUkcH2fSPy15JgqzqO71n6/SOhNxqIPQXRH5gSHrFYH37vWOSDnuqwmoKjajBoMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUevzhlsrgOtQ8LOwRBsGN1Pk6dEEwHwYDVR0jBBgwFoAUKQ3ogB8l0b4dI6fWlLD0WdO47VEwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwCgYIKoZIzj0EAwIDSAAwRQIgCFrR3oKDH2v9LWK1zDMIIumPo512ntcfJDpz2KyRPgYCIQDseuNiwsTheN2xQDo6Cyg7uHkjORuxURhQCcoQVyGgpjGCAd0wggHZAgEBMEgwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCwYJYIZIAWUDBAIBoIIBJjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI0MTAzMTEzMzE0NFowLwYJKoZIhvcNAQkEMSIEIPaye/IOiatxE/qZVu+5IGfOsrb/pw5s6EXgMxbHUkGjMIG4BgsqhkiG9w0BCRACLzGBqDCBpTCBojCBnzANBglghkgBZQMEAgMFAARA+uaTVxbYgF5mdEBZiW3fmtlEv57jdNRDoLZyoY4QZhkZLnB7djPjqkzt2KN4BmSfUlVLSs5/pbVOwVGvB0Ck/TBMMDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAgRHMEUCIAuefPhxtIeTzcfmcD2/sct018RGQkUZf6cd/gZbhOUTAiEAyak2wS0GccGyLKmOzLf1qhFPlg0xNvWFsYPXaaU2L50=" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAJLoYqDHwJYORZm20RVvq1WgprF1GA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3DCCAdgCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgJnZkV/5vs89xvZkEe5mY3EPlzMffcAAEzWay+Y5NXfswgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEYwRAIgdfSJnnbg7HItEVtXGJXRKv+mX42wf1+kTWfsK5+RwVUCIFmk0MpQQEvtppMyNpi2aREMgBlyBLZNagy2oEBZsmAJ" }, { "signedTimestamp": "MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAIg36qKQS62FnKKyVXngSdPZyjLVGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgdIRlDQniLDG+PQC0nFDn+yM7DWnj0HBpgl8vLl5btNQwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhALJZdk70DlAv/ponk9yDbUX8ua5gwFkcL2/F9ITMsL78AiEA03RK1OQutx0a3gEOUT4F7OR8/+/jURQB6AEP1UN9ne4=" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUYnPiMXSrHdM9jSdGvG8SFy1PFBMYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDIxXh6UbkxROrCcmXMMtV5YT4xoVPONPKgdsqr7i6yvjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmrEaUHQaDJyJ3SgOfT6zEaNoDzWJmXPEZGrTPcndvawCIB+fblhJghtC/A/3z3vBth6eNMRAyUPmgMIrIZNockc/" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAOhMcTpiZiwek9RMeTKHogZlXH5aGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgiW9TS7B6Pj2i8/kOsyc5J6Nlv9B2cs0TmJrKXGiezRAwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgI6iJSt0G769QYyAanhmt3vhdbPCH9XlduqugeMVftZ8CIQDZhCHQ1IlkWwqjqVbdsiyT/g/H0osY8njsNhsU0avyTA==" }, { "signedTimestamp": "MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAKTrtFND23NJeB35euXQDT3Mv+cnGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgoARyH9dgZX1lK113tbvVKZ2ItQWbKqOPQdCBzktaxMQwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhAKZPGGj2Jtjmj4ajvcWuZu/DJqqZ4NDdyTL7Sw5H//XcAiEAnnNpf5gV7he/SK66ptbSM1nI2XkuZvmX+Hc1IS/Sbd8=" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUQds0NUv3YD/AIhweumMRsB9HJMMYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCEU+ebNhMRe5LiFrrd0IwYaWYKY58oFoZ/zHukh2nViDCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiB+R3VFt3iQGLzYn7EMWMcgW+UiNODBKvLS7nJJrVPBkQIhAOLmBBdu7+ovvZvtZAS+UySWmM/Z9QHOIkYZ+XV3Ej1S" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAMGxKPwtQffZvcj+aXlbzmEujPf7GA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgpzeKv5SFb+Ws6pe2Jduo6OZfuilL/ImxrPw4ld9ndLYwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgRbw9d+eJwF/XasG6YSqDp6/AoJGbuUEUOXPQajwwBg4CIQCgTqR71HrpMeYdH2kKdrs6GlC6vPn+YvWAJHSTrWMUyg==" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUIyUZu6SUFnvc9pDQMVrMd0lYtywYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCHUlruf/vA80MakZUrOnhdzFYjSrM6ip1NfOUCYJ3jLTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAs4ykr1TcxjtFAGUX8w/+i2Mb2QHBKOoCm7Mus5ETzBcCIG0FxCjjbOkXw+EQtEaGl1FxISmI2h7HCCQ5G6l/ydY9" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUbTGN52J6iZc7ceiPM8aRd9aiZZAYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCAbSxLrzt+SZjAc5ZcchbyrfWfLq5G3H/xmdA0WD/PwTjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiBfP/ENsJrxKQXYFma4b3PqFpnroHA0fh3dAOi+6ud7agIhAMSjqykzqraZ1v3fZkW7ehF4ybZsnulwbCoho1c0DGHB" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUI0Uj5oFX3HIBcrNERp3Lg3e6HcoYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDe32jIZ6h1TaxrXNUHcjUy8xDINcNt8ZKPw/PbEvhDTTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAigUZ03YjBk3IgQAR7lW3fYvBedhZwCjo1hgbtvEFlKQCICBqO3wm8IU6Iku1I+1+cZphdP8BJzSfFsW5eeDUySoW" }, { "signedTimestamp": "MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAMYTttbsf5TVXLWLsskIChkdBukvGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQg6FCz+9D0Mmk2b0liV/c7PMoEZ1XJOMAGL5RKYyNdaiowgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhALS5vpv8wh0u+lrPU0iTIeKkZ9hvYUmsmsTy+vhN7jt2AiEAmZiJUy5iRu8kA5NtNlbyxVD4G4nQPTVG8+KpJSypg1g=" }, { "signedTimestamp": "MIIEyDADAgEAMIIEvwYJKoZIhvcNAQcCoIIEsDCCBKwCAQMxDTALBglghkgBZQMEAgEwgeMGCyqGSIb3DQEJEAEEoIHTBIHQMIHNAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwITLeOwPutMmIy9ptwVRrrV9iHizhgPMjAyNDEwMzExMzMxNDRaMAMCAQECCQDeEKL9dsIkd6A0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZ6CCAdAwggHMMIIBcqADAgECAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlMB4XDTI0MTAzMTEwMTY0MloXDTMzMTAzMTEwMTk0MlowMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFsBczJAcgy5m1djVteUhYIoM8q2zWPUkcH2fSPy15JgqzqO71n6/SOhNxqIPQXRH5gSHrFYH37vWOSDnuqwmoKjajBoMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUevzhlsrgOtQ8LOwRBsGN1Pk6dEEwHwYDVR0jBBgwFoAUKQ3ogB8l0b4dI6fWlLD0WdO47VEwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwCgYIKoZIzj0EAwIDSAAwRQIgCFrR3oKDH2v9LWK1zDMIIumPo512ntcfJDpz2KyRPgYCIQDseuNiwsTheN2xQDo6Cyg7uHkjORuxURhQCcoQVyGgpjGCAdwwggHYAgEBMEgwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCwYJYIZIAWUDBAIBoIIBJjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI0MTAzMTEzMzE0NFowLwYJKoZIhvcNAQkEMSIEIL4Zc9dFtdmbazGRScylYeV9frTjeTjZ63HSKXwJLayEMIG4BgsqhkiG9w0BCRACLzGBqDCBpTCBojCBnzANBglghkgBZQMEAgMFAARA+uaTVxbYgF5mdEBZiW3fmtlEv57jdNRDoLZyoY4QZhkZLnB7djPjqkzt2KN4BmSfUlVLSs5/pbVOwVGvB0Ck/TBMMDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAgRGMEQCIF54Y6QhPoj0n7E5ZCU1WnZ1c3TYOwnX2WQRD6R7WD+SAiBy+iSKWo444zGVv2AxLaHG0lp5r1CvTAMqPCE4f7uDuQ==" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUTIPW51Kzlup9NpJuarjVlQkXpugYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHeMIIB2gIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDs4QjBqIMyPO2fo/b8SjlNVDPNvaV5li54i43cA2nvRjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIESDBGAiEAtyu5AUpVmpPwaIEZe0mi5o3UjSgRMcRt6W1tbL7EMT4CIQDT4ddVhhtjKhf5opJ6dD/UXQ14xlrddDBZ9+0jsaQLcg==" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUeNmWsBH4ky6lc4VUxeGAzPs6V+QYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCA1NtyjF3jlgAqkHfJDfyb1+D/UOaABhJfm0RhcvpZIvTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAp2Xys7Tfl/WJB6ZFcMxMn3VhLc6MvNTORBHVi3CJhMsCIFfJ3PBILVeko5Qj1tybiqYL8aNKXMIcm5dv0sFVbRLv" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUEFKQvJTgA1QyuhDzBJMqlB7FIeQYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCB4pQo8/QYvXbUleCV6iFElReti9ExJVWTxUAih36suEzCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEArhAhQ1FYvofLmUmAzk8v6ErYEsCE2vngpGoNkmkybZUCIChWYnbfEwZDGTCebuwenfW0ndqFjJbnpcDXArp590NH" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAKmQSfy4m28Bl+diEK6dclIivqAPGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgwIZoyHcKEnjlV5aHtlhKd2uJ7heBcbA4YuSpQZZ5RkYwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIhAL2Nm9AiCE/6bE3X4zxrJSP5hHXFe/f4p6eenI9tNEgOAiB/cxrD69xFEW6N33wvgDCRE8Q0XC92jKxWbBamgPyjtA==" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUWRjdmZLHICtEm70NbMoe3Hm316QYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBK1V9osV6v0ngq5pt5oslEqPQh6NnCTimd5yTtUeVFTTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiAHwVYpCB+br0HeRKTtKJZAdzTaRj6XR72i287BTl3/IwIhAMrra0CD+s+X26mK+BDxBozV4lgpW4ZXLzbL/rC5Mbmx" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUaGDqxc9Rx+1uPcarTyB3gCaSABAYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCOj1ozKtwZFgeqJ48LSRwvQLM27AkujSeXof8BzRuWeTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiAoIOACPZBemfDo9JCi8f3AD9fLJjvoVNvPL9N1OJFnmAIhAKCUqP+i3pJRrizarje/wUS0/iaOXYh3lLUjETrgVSCB" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUUbP7pOissqOStCSLp3EFafJEtYMYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHeMIIB2gIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBaWur+DlUeVjUCEpwpXJe3Iq9ba+oMdn6qptW4N2nz7TCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIESDBGAiEAi+cZ8bda1ZHjdhc7ikjwFLaHXw2vDwAQSHGOFpSttzMCIQCBwZgA1XRLEq296IAH4TyNZFRxoxnGGnqWEY7oTfvuCA==" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUQsy8bqxaU7nFhuDvs0zk3Kr2680YDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCAjYjcQaMh/tc3YM2g6W0Z5bQAO/uBkHOXR5WbyI/gEOzCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiA3j2dbOrHG6OZEIEOAxe3Usb+7eLfrvnSSFgE+Oa6gwAIhAP9PAQlUqrEfOL7aDbiD+hglBRDJ9RPExRW55+Pn3JSN" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUCemJTTDrv0Wsd9IaZDxMJNYwTwYYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCTT+cKL9bCmR0gv/53jlas7I+VVxCVCp/HwI6BhhbK5TCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiAaKKMCyJkhh5paSin5JQDWlo+5N/Ca5IIpJcIDV0t9yQIhALnC230cQdM5Zgb02iwGyWYrLw4ib/6qMoFzz5gipRPz" }, { "signedTimestamp": "MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAK18cM7JkaNz2uMeXml3idd62ff6GA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgEM/0lJtEhZJ87BF6E2NW7g58LBMYJhEkHkzX+pSbTykwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgAi0YPVA7nVjTmw4punVccQlIuxOSQrL+03CpHSPY3lMCIQCaXUl5aPoFM2263M1Fi37a87KCvc0UyZTWDGV6gbnPcw==" } ] } }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=" }, "signature": "MEQCIHakaxFa7vZAGWMK1euuc26LXcztTrDyI2OSf7YF5qE6AiAi/U3Uo4zGDn++Ze9iR2Dps3lISEzCNCfeBrsEm1XGiA==" } } ================================================ FILE: test/assets/tsa/bundle.txt ================================================ DO NOT MODIFY ME! this is "bundle.txt", a sample input for sigstore-python's unit tests. DO NOT MODIFY ME! ================================================ FILE: test/assets/tsa/bundle.txt.late_timestamp.sigstore ================================================ {"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": {"certificate": {"rawBytes": "MIIDBTCCAougAwIBAgIUIs3M2DgogCj3KotUVZg8Mok6IhMwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNTEyMTg0ODI2WhcNMjUwNTEyMTg1ODI2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEn2elp6N4BmBpOaQbpbiYY5EBXJq5+f0tPnffeJTbLVzPgUbpX4T5ZS7KDuQFQSPrljgIZAO3+ZmFSFFnwVrNv6OCAaowggGmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU+j0g8S3mHrEo3eautm7T4RnwWwUwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wWQYDVR0RAQH/BE8wTYFLaW5zZWN1cmUtY2xvdWR0b3Atc2hhcmVkLXVzZXJAY2xvdWR0b3AtcHJvZC11cy1lYXN0LmlhbS5nc2VydmljZWFjY291bnQuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABlsXTr40AAAQDAEgwRgIhAN790LnuqDZwfqzyilH4qtk7zVvVZUQXB0Q0YfX9tNWXAiEAzH649BUx15UYsZUGihsBfNUQXov87UYzfYE2Zw2L174wCgYIKoZIzj0EAwMDaAAwZQIwCJ8+cVdfOc5SPoQnjY6rrIxIlYqLgtW65YrX8GzbRW4NpP37m6nxi6cjqtgwGFMeAjEAp4JgaETMFRgSBSSZLB7uhqr1fY97LPcHmAebKFpqFQERELMUmmqk5uHB2wgtvzB2"}, "tlogEntries": [{"logIndex": "42066373", "logId": {"keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1747075707", "inclusionPromise": {"signedEntryTimestamp": "MEUCIDxpagNcBytw52lZI3CwbTA6lfydnHlIGogI1Jfu13PKAiEAri9BAnNJDCZV3gEj9MuLEPw6jVbAiKfmrUmoaAIqSsc="}, "inclusionProof": {"logIndex": "10383961", "rootHash": "K6ZWztp1qbjIuzexDwMUOhf/+S+wqz4iQEDFTcEnNGQ=", "treeSize": "10383962", "hashes": ["HjQ6YJcBoxxKm4Uxs0zJCqC/LM/phnZMGiOiDLXo5wg=", "HSzuxscITh6g9k7vt64/9Z8zPwGwcQJv7NfnX92ULng=", "EEVPMqL5AIgHaYl2NbjmSTvn31oGEjhpTPbpgowrPM4=", "WbnH9wLRq4lD3Ju3FWOBZ+PEfvXT2c0Ugqy78gFgR0M=", "Kv0MBtfoWuGMfuJhPQiwSV7qUt+ALTQMx9BWYUrusb0=", "vld+lIPewmCjCp7W2cwUZD59sPgCK0rC0T2GpveXsmM=", "//dvSvxZzO+sVgkN0WfDdWVO4VGUsVGNT6bSmn5b/Qg=", "AzkFO8X9eKMNJxy+AuVjKe/2ObuNc4pGFzYucDuH87I=", "BVBT6KGWJPrAI4T7Zzt529+ZxU+G5UR0UMqPDcKUYzk=", "1CmXahercpSNPyH2ATDpK8S80Gim/GrKkm/8V5Ozue0=", "ZyAV6AeFLhv6n2Ya599XWHwy3HCr/y0+RF0P6Smg8IU=", "GoHRwlhYuJIYJdmRnHX5HWLr2ngxzHnAIIqBewovBi0=", "OdoqbUqBYHhj2W1RLM8APkQOnM2K9gzGm1KPFmwIIeQ="], "checkpoint": {"envelope": "rekor.sigstage.dev - 8202293616175992157\n10383962\nK6ZWztp1qbjIuzexDwMUOhf/+S+wqz4iQEDFTcEnNGQ=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiEA+12tjmkJ2CeZlW4baTsLtnVfdSeWNyW8ZFykmBcAn4QCIB6OZTD/bVgAsuq5FgSQZzwn0RPYl7+S1IFRYAoHIP5G\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRmhsZU1oMjlRRW5QbVc3Mjhkd1h1ZkdGTVo0NG8zNkNseGVxRWVWaUxSdEFpQjIzUkRHenArbjF3aDVjVTF0cC9CampIc3RBQjdsWmY5S0tKbnpwM3ViV0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVUkNWRU5EUVc5MVowRjNTVUpCWjBsVlNYTXpUVEpFWjI5blEyb3pTMjkwVlZaYVp6aE5iMnMyU1doTmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFZkMDVVUlhsTlZHY3dUMFJKTWxkb1kwNU5hbFYzVGxSRmVVMVVaekZQUkVreVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ1TW1Wc2NEWk9ORUp0UW5CUFlWRmljR0pwV1ZrMVJVSllTbkUxSzJZd2RGQnVabVlLWlVwVVlreFdlbEJuVldKd1dEUlVOVnBUTjB0RWRWRkdVVk5RY214cVowbGFRVTh6SzFwdFJsTkdSbTUzVm5KT2RqWlBRMEZoYjNkblowZHRUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlVyYWpCbkNqaFRNMjFJY2tWdk0yVmhkWFJ0TjFRMFVtNTNWM2RWZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDFkUldVUldVakJTUVZGSUwwSkZPSGRVV1VaTVlWYzFlbHBYVGpGamJWVjBXVEo0ZG1SWFVqQmlNMEYwWXpKb2FHTnRWbXRNV0ZaNldsaEtRUXBaTW5oMlpGZFNNR0l6UVhSalNFcDJXa014TVdONU1XeFpXRTR3VEcxc2FHSlROVzVqTWxaNVpHMXNhbHBYUm1wWk1qa3hZbTVSZFZreU9YUk5RMnRIQ2tOcGMwZEJVVkZDWnpjNGQwRlJSVVZITW1nd1pFaENlazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVGeVFtZHZja0puUlVVS1FWbFBMMDFCUlVsQ1FqQk5SekpvTUdSSVFucFBhVGgyV1ZkT2FtSXpWblZrU0UxMVdqSTVkbG95ZUd4TWJVNTJZbFJEUW1sM1dVdExkMWxDUWtGSVZ3cGxVVWxGUVdkU09VSkljMEZsVVVJelFVNHdPVTFIY2tkNGVFVjVXWGhyWlVoS2JHNU9kMHRwVTJ3Mk5ETnFlWFF2TkdWTFkyOUJka3RsTms5QlFVRkNDbXh6V0ZSeU5EQkJRVUZSUkVGRlozZFNaMGxvUVU0M09UQk1iblZ4UkZwM1puRjZlV2xzU0RSeGRHczNlbFoyVmxwVlVWaENNRkV3V1daWU9YUk9WMWdLUVdsRlFYcElOalE1UWxWNE1UVlZXWE5hVlVkcGFITkNaazVWVVZodmRqZzNWVmw2WmxsRk1scDNNa3d4TnpSM1EyZFpTVXR2V2tsNmFqQkZRWGROUkFwaFFVRjNXbEZKZDBOS09DdGpWbVJtVDJNMVUxQnZVVzVxV1RaeWNrbDRTV3haY1V4bmRGYzJOVmx5V0RoSGVtSlNWelJPY0ZBek4yMDJibmhwTm1OcUNuRjBaM2RIUmsxbFFXcEZRWEEwU21kaFJWUk5SbEpuVTBKVFUxcE1RamQxYUhGeU1XWlpPVGRNVUdOSWJVRmxZa3RHY0hGR1VVVlNSVXhOVlcxdGNXc0tOWFZJUWpKM1ozUjJla0l5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn19fX0="}], "timestampVerificationData": {"rfc3161Timestamps": [{"signedTimestamp": "MIIE6TADAgEAMIIE4AYJKoZIhvcNAQcCoIIE0TCCBM0CAQMxDTALBglghkgBZQMEAgEwgcIGCyqGSIb3DQEJEAEEoIGyBIGvMIGsAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgLenXcJBzdjua5V/WDyNWpTIysBnS9xKUPS0plFLqG0gCFQD3x9GVccz7Cvui6lxEdDQtb7L3uBgPMjAyNTA1MTIxOTAwMjdaMAMCAQECCHSCL66M6EByoDKkMDAuMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxFTATBgNVBAMTDHNpZ3N0b3JlLXRzYaCCAhMwggIPMIIBlqADAgECAhQKNaEGYdXiQXPGiZan8n3yfgN8pzAKBggqhkjOPQQDAzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkMB4XDTI1MDMyODA5MTQwNloXDTM1MDMyNjA4MTQwNlowLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATHW/kXcekP16Ae6SekEWVHPtAFEMm7hp5XO33MktFjSW+bHWUXtYEzZz0A3xkY9CyYOoeUk3ZH/v5HEuS+UvORzX0g7Hfy3uYYYRwHtqBQN0IX8rLdFMtIrRej/QCAdB2jajBoMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUqPxk9ijeLuY7c09UjFLE4ZzdU6UwHwYDVR0jBBgwFoAUOyBGWV61Mk1HMM5uY+5zdEfyBH0wFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwCgYIKoZIzj0EAwMDZwAwZAIwRK9VLoYa0Xff4nX1N/AQ1YleNG/iLT8dAXAtRKRfpN9XuDScbxWeo0cku8SkC06NAjBQPe7LBNeitA/UOBtXT2sX1h6f4ISqz+ISmJ4lY+y3bzRJI5nk1r53I9WT3/xIWToxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFAo1oQZh1eJBc8aJlqfyffJ+A3ynMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MDUxMjE5MDAyN1owLwYJKoZIhvcNAQkEMSIEIB+SgwjYmkSbLhZNvWnGj/KrNAOr+sqpO38OpoIYSOSZMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCAG9P/gR/6zWZm3M7DXoyNQHPwY5MAzZqhF13U250snRDBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQKNaEGYdXiQXPGiZan8n3yfgN8pzAKBggqhkjOPQQDAgRnMGUCMFMwn1mx1D3q+vKwf57UDA96286zoTJ+ITJG5IQVypKLqnKSEX8Gm7GIRDXR06PJPgIxANj1zJ+cVXxoYuH4H8yobeqVeztGLZNd+YqbkyuvTkcX46CTCH0e6imE+Z4yTCRiYw=="}]}}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4="}, "signature": "MEQCIFhleMh29QEnPmW728dwXufGFMZ44o36ClxeqEeViLRtAiB23RDGzp+n1wh5cU1tp/BjjHstAB7lZf9KKJnzp3ubWA=="}} ================================================ FILE: test/assets/tsa/bundle.txt.sigstore ================================================ { "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { "rawBytes": "MIIC2TCCAl6gAwIBAgIUdmztZIKhChYc16oLF65pX34wgpowCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAyMzM5WhcNMjQxMDMxMTAzMzM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6jFpMi07y77fdwwYmgZ8mMsiORhq9OYO/1KtrJJFHl1yrnN6hpX7vC5affuipObcL3utSgCAnwN1QCAfumx5VqOCAX0wggF5MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUaMSROcZrZvwW7N6tp6yjzkI5QmkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGS4hotJAAABAMARjBEAiB3YxcguZbssCo28dz3BTlBf2RNwL3GOicOIecLahdeJgIgA0RNy/ARrGW2iAnM1PWT/gBgHcQ+wk0hD4FFAmM5JrYwCgYIKoZIzj0EAwMDaQAwZgIxANwxTWEcb9oFkCo63tNd8/ueYAKpsowGyyQs+AX0CE0XJiHjc24HT57G9CP3XYRCnwIxAITQtm0+VvPufhJGvMtn6K0okqWWZFFJQrz0akRlBHHk3osCdhENY0ZBmT8f+59b7Q==" }, "tlogEntries": [ { "logIndex": "35355462", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, "kindVersion": { "kind": "hashedrekord", "version": "0.0.1" }, "integratedTime": "1730370219", "inclusionPromise": { "signedEntryTimestamp": "MEQCIFWlAKfTUTVLdRAkICb7QjK9wWa5clIPSO/I2as7NemMAiAptKOQSwFZsdM/T36yjDhXu4i4i32iy4mLDKFH2SBmAw==" }, "inclusionProof": { "logIndex": "3673050", "rootHash": "CRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=", "treeSize": "3673051", "hashes": [ "PaodjVERCZrJ4m+Ux1vKwci70JNV1o7i6tg+r7emiLU=", "hb5Kc++ml8xcjeNY59TfzSSnPGhTQqnl+7VhO4Vr6a8=", "pVIutklD+cs4kcBFMp3iPbw/Kn/rWtdwTHwh87zm/so=", "eUTldsq4LV/OSczlwUFHxK6yY1+kE/ASoidYXY1zybw=", "2rA1/K1G+of0n4dAsYaj4AlV4MWHM7CJz24RmIrEfhs=", "P8eXf78ohkRkntQNFfarUtn9Gct7yy+smjM5cersyUg=", "3Ul1Loa16XnnGTifeAYy8nlO0JyNIL6E/ZWE1tuIE9w=", "mU9v3N0cr/U/8VEM8R56E8z5ScHbeALqtChTUlAmTr4=", "70FF4PlelNUMSWeGPKROonP6S+1hpHMe5r5uwLPhuro=", "ZS9WKtLvUQYFzFNmaQP+2Gtstl9yM3150pk+oqIMMHU=", "lRbgwAuY5l5kOuRQN6uQ8zRQJ5ntgvHUCcNOBOI4Wyg=" ], "checkpoint": { "envelope": "rekor.sigstage.dev - 8202293616175992157\n3673051\nCRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\n\n— rekor.sigstage.dev 0y8wozBFAiAwPJa5KEL421/AQF8uo81cctm4t9lIY6IGmeH2fV9d1QIhAM6j+/flHM4dEyf5sKCNwyKt9nb9DBLlTHDsPOIrTkyQ\n" } }, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGFrYXhGYTd2WkFHV01LMWV1dWMyNkxYY3p0VHJEeUkyT1NmN1lGNXFFNkFpQWkvVTNVbzR6R0RuKytaZTlpUjJEcHMzbElTRXpDTkNmZUJyc0VtMVhHaUE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXlWRU5EUVd3MlowRjNTVUpCWjBsVlpHMTZkRnBKUzJoRGFGbGpNVFp2VEVZMk5YQllNelIzWjNCdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFFVFhoTlZFRjVUWHBOTlZkb1kwNU5hbEY0VFVSTmVFMVVRWHBOZWswMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUyYWtad1RXa3dOM2szTjJaa2QzZFpiV2RhT0cxTmMybFBVbWh4T1U5WlR5OHhTM1FLY2twS1JraHNNWGx5Yms0MmFIQllOM1pETldGbVpuVnBjRTlpWTB3emRYUlRaME5CYm5kT01WRkRRV1oxYlhnMVZuRlBRMEZZTUhkblowWTFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZoVFZOU0NrOWpXbkphZG5kWE4wNDJkSEEyZVdwNmEwazFVVzFyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDB4bldVUldVakJTUVZGSUwwSkRVWGRKYjBWbldWZDRiR1ZIYkhwTWJVNXZXVmQ0YzFsWE5XdGFWVUl3WTIxR2NHSkhPVzFaYld3d1kzazFhZ3BpTWpCM1MxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVXSmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5RM05IQ2tOcGMwZEJVVkZDWnpjNGQwRlJaMFZJVVhkaVlVaFNNR05JVFRaTWVUbG9XVEpPZG1SWE5UQmplVFZ1WWpJNWJtSkhWWFZaTWpsMFRVbEhTa0puYjNJS1FtZEZSVUZrV2pWQloxRkRRa2h6UldWUlFqTkJTRlZCUzNwRE9ETkhhVWw1WlV4b01rTlpjRmh1VVdaVFJHdDRiR2RNZVc1RVVFeFlhMDVCTDNKTGN3cG9ibTlCUVVGSFV6Um9iM1JLUVVGQlFrRk5RVkpxUWtWQmFVSXpXWGhqWjNWYVluTnpRMjh5T0dSNk0wSlViRUptTWxKT2Qwd3pSMDlwWTA5SlpXTk1DbUZvWkdWS1owbG5RVEJTVG5rdlFWSnlSMWN5YVVGdVRURlFWMVF2WjBKblNHTlJLM2RyTUdoRU5FWkdRVzFOTlVweVdYZERaMWxKUzI5YVNYcHFNRVVLUVhkTlJHRlJRWGRhWjBsNFFVNTNlRlJYUldOaU9XOUdhME52TmpOMFRtUTRMM1ZsV1VGTGNITnZkMGQ1ZVZGekswRllNRU5GTUZoS2FVaHFZekkwU0FwVU5UZEhPVU5RTTFoWlVrTnVkMGw0UVVsVVVYUnRNQ3RXZGxCMVptaEtSM1pOZEc0MlN6QnZhM0ZYVjFwR1JrcFJjbm93WVd0U2JFSklTR3N6YjNORENtUm9SVTVaTUZwQ2JWUTRaaXMxT1dJM1VUMDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==" } ], "timestampVerificationData": { "rfc3161Timestamps": [ { "signedTimestamp": "MIIEgDADAgEAMIIEdwYJKoZIhvcNAQcCoIIEaDCCBGQCAQMxDTALBglghkgBZQMEAgEwgc8GCyqGSIb3DQEJEAEEoIG/BIG8MIG5AgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgF2fxhmJk3kyzmUDGBhn8kEIYUvISuRhDjuVtN7jtKFsCFCfDYd/d4RagLKlLgkIpKC2V2+RxGA8yMDI0MTAzMTEwMjMzOVowAwIBAQIUN08D3ZVEkDYmzP0I9qnAMAsttoWgNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggGoMIIBpAIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCB8zAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI0MTAzMTEwMjMzOVowLwYJKoZIhvcNAQkEMSIEIIwtSBR2KyJoDjyGAgwVokItIcv6NZXcq6WsrdZ7xm2eMIGFBgsqhkiG9w0BCRACLzF2MHQwcjBwBCB6Z+5bJvtyJlSFYWzFldMxC5t4LAmOKRAO8Y3HALjAOTBMMDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAgRGMEQCIAY58TwPQDEm1hewp/Vn7ovaby/hnPzwxzRQulfj7+wvAiAAsjqb+meU6oJVgYia9YWVxeMAC+27c4NgtZsn3lXCJQ==" }, { "signedTimestamp": "MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJK4QC2mcxSdWnCqd1bF0JQo/lgsYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDFl7LJGj2Ly6mrN3rzvyj+h0hRUK4/mvHEgTXCpy9K3DCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlNj866pok1LTFRuzxIfu+h/KJ/kHmKnUfNF4PL2cdgsCIEKRudmJVaifKu72aNwiMB+P1YicRzgl/QGQPNAYDxUe" } ] } }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=" }, "signature": "MEQCIHakaxFa7vZAGWMK1euuc26LXcztTrDyI2OSf7YF5qE6AiAi/U3Uo4zGDn++Ze9iR2Dps3lISEzCNCfeBrsEm1XGiA==" } } ================================================ FILE: test/assets/tsa/ca.json ================================================ { "subject": { "organization": "local", "commonName": "Test TSA Timestamping" }, "certChain": { "certificates": [ { "rawBytes": "MIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKY=" }, { "rawBytes": "MIIB0zCCAXigAwIBAgIUGnqrcxtrSpsILclUa/+bCnYeZOUwCgYIKoZIzj0EAwIwKDEOMAwGA1UEChMFbG9jYWwxFjAUBgNVBAMTDVRlc3QgVFNBIFJvb3QwHhcNMjQxMDMxMTAxNDQyWhcNMzQxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2apBvcj3qtsHACafJaOd5Zw874AKK2s5XXdd6jrlVF9h3S6JFgUZ/5MVpYWDNKjgrkqbvhU3RroOGXJ4DyPGSaN4MHYwDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCkN6IAfJdG+HSOn1pSw9FnTuO1RMB8GA1UdIwQYMBaAFO0kKGzBCz7EddTsYBcuwp1VgbhxMAoGCCqGSM49BAMCA0kAMEYCIQDQutW0fTsKlGN4CohrIi/5fMIOqXpjxXswhxiBfCUa/AIhAOe4rlnAGQlmYlBW1uDqt0lw3a/2oAGvHRhDKbiIMPqo" }, { "rawBytes": "MIIBkzCCATqgAwIBAgIUfHAOxJRvpMlmRi3vt7yebkXSb9IwCgYIKoZIzj0EAwIwKDEOMAwGA1UEChMFbG9jYWwxFjAUBgNVBAMTDVRlc3QgVFNBIFJvb3QwHhcNMjQxMDMxMTAxNDQyWhcNMzQxMDMxMTAxOTQyWjAoMQ4wDAYDVQQKEwVsb2NhbDEWMBQGA1UEAxMNVGVzdCBUU0EgUm9vdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDQ2pO3oB5x2HKXp1YpgHB7SCVD1pag46/QUGfQHpyYWOdO4q7uqSx19f2StEszzqrZvpRioo1j6Lwnpp6oQ4P+jQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTtJChswQs+xHXU7GAXLsKdVYG4cTAKBggqhkjOPQQDAgNHADBEAiAFbrVtlmebUMEzUL6JijgYrZhkjUR9VvNO+J2rbQ2eeAIgHUf+TJCnYoq3In8hUlH4D92Fc3Xad6lI0mLfYWm5wpk=" } ] }, "validFor": { "start": "2024-10-31T10:16:42.000Z", "end": "2033-10-31T10:19:42.000Z" } } ================================================ FILE: test/assets/tsa/trust_config.json ================================================ { "mediaType": "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json", "trustedRoot": { "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", "tlogs": [ { "baseUrl": "https://rekor.sigstage.dev", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2021-01-12T11:53:27.000Z" } }, "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" } } ], "certificateAuthorities": [ { "subject": { "organization": "sigstore.dev", "commonName": "sigstore" }, "uri": "https://fulcio.sigstage.dev", "certChain": { "certificates": [ { "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" }, { "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" } ] }, "validFor": { "start": "2022-04-14T21:38:40.000Z" } } ], "timestampAuthorities": [ { "subject": { "organization": "local", "commonName": "Test TSA Timestamping" }, "certChain": { "certificates": [ { "rawBytes": "MIIFRTCCAy2gAwIBAgIUCmn2Vl7XF50OeM7Y1oM/vFAFK3kwDQYJKoZIhvcNAQELBQAwMDEeMBwGA1UEAwwVVGVzdCBUU0EgSW50ZXJtZWRpYXRlMQ4wDAYDVQQKDAVsb2NhbDAeFw0yNDExMDcxNDU5NDBaFw0zMzExMDUxNDU5NDBaMDAxHjAcBgNVBAMMFVRlc3QgVFNBIFRpbWVzdGFtcGluZzEOMAwGA1UECgwFbG9jYWwwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDiSD0m8J8ZGMS/Et2SqooLUCpwiAX9Ay/KOYCMFXr6ujAKOZGPQgASY7kdB1zA+dkHmoOxbF8kASVoFlwYzgnAvH9YpiVT9iVCRgF/sAutbdHYmOtPLpyB15PPiwnxB/PIk1d6e/WOh0Vn2ZX0juVJKb2B59FpzG1CXn9WGT7C1qVpXM+UtdRxxpug/lLBeDle5Uo/ffxGZfy5FsdlXTCFzkiqjf0cEIIxoEHxhOjxGjt2pPDuq7PLV0N0AWIhu7FU29fUePsS6TTk+8OS2Z8XQn8YHmgQMgqJF4fsv0ytTsNv5qPV2NEUi9Em7IemFFnfW5HktazmrqF7Ly/YPVv35X9zgT898YAVgd0+PaUqVgWEWv/hpV6kmXoNTxCcMqixbNQGxVWT9N5EMBZgc9yXesKFpHIb7cF/diloytxBOvnwm9PShBz6/KOfq17WPvOqK1UC4fMmdzppaXDuhOa4GhNoPUeo646oMFafpSoR1HG6Fom71oIxJ8Q63IxAFRdoKyioBlTuPDFXgIk3Ckv3+PVkJIl1imF33tnYut5OF+pMbrlf4I2Op4+n0CDsmRg9BBQBxIXoP2ziRIputnISW7uS55ViTkfO7mZRBIJzz2ZqX9igCkTvA1wMZzLeRbow2tkwRaTHYg4uQTGJuWMAkJRz27FjswVu1dIC27g7uQIDAQABo1cwVTAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQU6LppB2yaoGV18BzPLiq5NGHTecwwDQYJKoZIhvcNAQELBQADggIBAOAZhkQF9aC5oUA2cXtBRoYoXnPILrbdIvVQvfe3IOUAhF5QvjrVwtBffR8ZxrSSn/8t0rtXy6BZQas95YD0ObspemMocKSsPYo01qnuCX5757LSvTpYPERVt6TpKVV39Y59xlmuUjlyQH0Ufrsh8Qs8ejPQwDUWmDwetBnrZfDV36AgCAjlS3QSQQt+iYb6x13jYGZ9Wj/HUqSaUlJqqtuzbbIMZy8FSHLjle5m2Np2Wubwn3a3z+xTYVN+gWDFWtEamDprRxQ6oswXmINv8cZd79cMZbFS7j2Crnni58uVLxMQAcSNBEnQTChTdD6JzUjHiSzpaSTn/txfP9M/rMTSDokqPgfhpWcB93sw0X5Inv2nsqMN6U8b28F0+ciBP7dKVPTM8ypfVpAJ0OtGijkGda6cfYbcXCTTMZFAnMPenfVMN9TtljZ/lOdaNLuaRVKcOJvrHLqv4Mau+9TPkd8Xn5YWVCxtYr/xhKdaHfQ2KGr987CP6hKoIAPPIebQWjd1jrrlm1ESebcm1pGTWiNyGhKUUaFsKt96xmtGa3ov3OfcygSDGdPAIy5LlWyfdEX9rwoqTi+s6EELabj2C6ICCUYwqr6quaQrvhdJ84Oqs5Tn3hkcrroJtLPQBtYNGjHZtJLyXZ/wEAUciWTSyLinVABhBdXzTlUnwO9wkxdx" }, { "rawBytes": "MIIFPTCCAyWgAwIBAgIUEP4pDZweTUQeXvhyu1e4kJaJ9FAwDQYJKoZIhvcNAQELBQAwKDEWMBQGA1UEAwwNVGVzdCBUU0EgUm9vdDEOMAwGA1UECgwFbG9jYWwwHhcNMjQxMTA3MTQ1ODU2WhcNMzQxMTA1MTQ1ODU2WjAwMR4wHAYDVQQDDBVUZXN0IFRTQSBJbnRlcm1lZGlhdGUxDjAMBgNVBAoMBWxvY2FsMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4kg9JvCfGRjEvxLdkqqKC1AqcIgF/QMvyjmAjBV6+rowCjmRj0IAEmO5HQdcwPnZB5qDsWxfJAElaBZcGM4JwLx/WKYlU/YlQkYBf7ALrW3R2JjrTy6cgdeTz4sJ8QfzyJNXenv1jodFZ9mV9I7lSSm9gefRacxtQl5/Vhk+wtalaVzPlLXUccaboP5SwXg5XuVKP338RmX8uRbHZV0whc5Iqo39HBCCMaBB8YTo8Ro7dqTw7quzy1dDdAFiIbuxVNvX1Hj7Euk05PvDktmfF0J/GB5oEDIKiReH7L9MrU7Db+aj1djRFIvRJuyHphRZ31uR5LWs5q6hey8v2D1b9+V/c4E/PfGAFYHdPj2lKlYFhFr/4aVepJl6DU8QnDKosWzUBsVVk/TeRDAWYHPcl3rChaRyG+3Bf3YpaMrcQTr58JvT0oQc+vyjn6te1j7zqitVAuHzJnc6aWlw7oTmuBoTaD1HqOuOqDBWn6UqEdRxuhaJu9aCMSfEOtyMQBUXaCsoqAZU7jwxV4CJNwpL9/j1ZCSJdYphd97Z2LreThfqTG65X+CNjqePp9Ag7JkYPQQUAcSF6D9s4kSKbrZyElu7kueVYk5Hzu5mUQSCc89mal/YoApE7wNcDGcy3kW6MNrZMEWkx2IOLkExibljAJCUc9uxY7MFbtXSAtu4O7kCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwHQYDVR0OBBYEFOi6aQdsmqBldfAczy4quTRh03nMMA0GCSqGSIb3DQEBCwUAA4ICAQC+6pAcMuxSx32C69fxLYyUYvj6A66DsXuNnzY/CqgHzyD+vUF7oS1tfoU81BcjY9+cSQkCO6teBbsrjKpFmnpWf5grHWaW/qFf4+tg1i8oPnJ9XDPn9U12M9mYk/xK0GM7xXK+1dMfkrI50RUtIhfIe7N+OzBVtOtJIUoItSapZeXDvTtScf50XdS73kBlr7VFIrKlfAm3C+G+wL8MiMg1254srhtfvzP6RVPy/uUZRh+F8NWVMcAl3IrSsBkDdDHFbJcD+tHmN9NQ9I4/51PcStXFPpl0k6EvadQMZ6Ep6HHfsJUdfRIHWxP9BYwXURO7bmmlai9M+Do9LHY0lb8s9fGXkgi0p9aKgFZb0uLfqsrlQjFqpZOv3GFmcwXfc5IOC//1dJO6kL37nTiv4yHEzSzgbq6xyYEy6gJSo+Zgnd10f1y8fCXhzHFNNBNQHC6jvT63mo/RlH27zJHCHEvx39B9GwYRNEdS2MDSVuJ5RcVgA9E44LXxq++r9y5LvviC+aV5H9WgJOlJU0+ZSPJTSfAdY/MMqvIB+kelFCk32qQzAH9e2Nb4AF63aDEv6iIT39+A82ZWVZwTrAy0cPPNIfKuiUtQQ0m/yyuMVRme0ZesZYtTCx2879DzmIrYhng53xN34SPfos/cm0JqIwViJUqB5/cVNosj53uflgOJ7g==" }, { "rawBytes": "MIIFQTCCAymgAwIBAgIUOcx13OBKeYy2jy6faZcez0+NmQYwDQYJKoZIhvcNAQELBQAwKDEWMBQGA1UEAwwNVGVzdCBUU0EgUm9vdDEOMAwGA1UECgwFbG9jYWwwHhcNMjQxMTA3MTQ1ODQyWhcNMzQxMTA1MTQ1ODQyWjAoMRYwFAYDVQQDDA1UZXN0IFRTQSBSb290MQ4wDAYDVQQKDAVsb2NhbDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOJIPSbwnxkYxL8S3ZKqigtQKnCIBf0DL8o5gIwVevq6MAo5kY9CABJjuR0HXMD52Qeag7FsXyQBJWgWXBjOCcC8f1imJVP2JUJGAX+wC61t0diY608unIHXk8+LCfEH88iTV3p79Y6HRWfZlfSO5UkpvYHn0WnMbUJef1YZPsLWpWlcz5S11HHGm6D+UsF4OV7lSj99/EZl/LkWx2VdMIXOSKqN/RwQgjGgQfGE6PEaO3ak8O6rs8tXQ3QBYiG7sVTb19R4+xLpNOT7w5LZnxdCfxgeaBAyCokXh+y/TK1Ow2/mo9XY0RSL0Sbsh6YUWd9bkeS1rOauoXsvL9g9W/flf3OBPz3xgBWB3T49pSpWBYRa/+GlXqSZeg1PEJwyqLFs1AbFVZP03kQwFmBz3Jd6woWkchvtwX92KWjK3EE6+fCb09KEHPr8o5+rXtY+86orVQLh8yZ3OmlpcO6E5rgaE2g9R6jrjqgwVp+lKhHUcboWibvWgjEnxDrcjEAVF2grKKgGVO48MVeAiTcKS/f49WQkiXWKYXfe2di63k4X6kxuuV/gjY6nj6fQIOyZGD0EFAHEheg/bOJEim62chJbu5LnlWJOR87uZlEEgnPPZmpf2KAKRO8DXAxnMt5FujDa2TBFpMdiDi5BMYm5YwCQlHPbsWOzBW7V0gLbuDu5AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBToumkHbJqgZXXwHM8uKrk0YdN5zDAfBgNVHSMEGDAWgBToumkHbJqgZXXwHM8uKrk0YdN5zDANBgkqhkiG9w0BAQsFAAOCAgEARUUincNj6OEbvE0shsaKo4ZeffEgnSzTlSBdYASQeCs130lyXsQVwZkyL4IrPsICE9lYN57QvXEPXYi0d+kQvVdBSm4vmoWSZdxe6GEj5CJ3k4hb/uyEgcrvgUSO+33v3L/sRYfIax/8y+1oxSgFcmSml6hMmHlH0q9/Yjfsv6ys5iifipQrXOD9yBcvLIKHMovrVD+BCjirz1a1g5CneTePhLDNzk0Kbvqc+sNWEDlzQzmHjeKHgDTrJj1OcFpUfsZOrFMscXCGVVA/eB5YOrFbTvtKdzy7d9UN+/PUCqZt1dcYzlk75ww2bFgRXt1GhzUqRolblTRWeLmwIkjDpyRaA1C5MXhWie7XT7G52SoGSPzjSSvo7hPqO8eW1fHK/qv4LTxX1o2yVyKpsoeV/SSybbzwq7ZeGDBeMrfCXktQLFqDwqnGMjlJsx0MkKVaDOR9Y4dz6P9YlGo7qDamw6wwbNvsJRTNkeNQyfZPyBBDW/I+gK95EisTu2zblfT6ie64ckeIjvv7UxtRQFxEMWNoeMT5E3SZNOMH4zSbQGQhCtXg1s4ssS2w2AYJ8CRJiOGfe1Pa30zQVbOACXEYO0z9R1ED5xSRck93GIss2BVUL92+sdnk6JxJLKQH8icN3jX3dsM0i+dm1TxTW1flVZGGpR0xLbgRuNnQI4YCLOg=" } ] }, "validFor": { "start": "2024-11-07T14:59:40.000Z", "end": "2033-11-05T14:59:40.000Z" } } ], "ctlogs": [ { "baseUrl": "https://ctfe.sigstage.dev/test", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", "keyDetails": "PKCS1_RSA_PKCS1V5", "validFor": { "start": "2021-03-14T00:00:00.000Z", "end": "2022-07-31T00:00:00.000Z" } }, "logId": { "keyId": "G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=" } }, { "baseUrl": "https://ctfe.sigstage.dev/2022", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-07-01T00:00:00.000Z", "end": "2022-07-31T00:00:00.000Z" } }, "logId": { "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" } }, { "baseUrl": "https://ctfe.sigstage.dev/2022-2", "hashAlgorithm": "SHA2_256", "publicKey": { "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", "keyDetails": "PKIX_ECDSA_P256_SHA_256", "validFor": { "start": "2022-07-01T00:00:00.000Z" } }, "logId": { "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" } } ] }, "signing_config": { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [ { "url": "https://fulcio.sigstage.dev", "majorApiVersion": 1, "validFor": { "start": "2022-04-14T21:38:40.000Z" }, "operator": "sigstage.dev" } ], "rekorTlogUrls": [ { "url": "https://rekor.sigstage.dev", "majorApiVersion": 1, "validFor": { "start": "2021-01-12T11:53:27.000Z" }, "operator": "sigstage.dev" } ], "tsaUrls": [ { "url": "placeholder", "majorApiVersion": 1, "validFor": { "start": "2024-11-07T14:59:40.000Z" }, "operator": "sigstage.dev" } ], "rekorTlogConfig": { "selector": "ANY" }, "tsaConfig": { "selector": "ANY" } } } ================================================ FILE: test/assets/x509/bogus-intermediate-with-eku.pem ================================================ -----BEGIN CERTIFICATE----- MIIDHjCCAgagAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c Nivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT FcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E 7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4 hSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp gawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g KQIDAQABo1YwVDAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTASBgNVHRMB Af8ECDAGAQH/AgEBMAsGA1UdDwQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDAzAN BgkqhkiG9w0BAQsFAAOCAQEAhLUXPPHJrVnd9G3OQ95uJSWdzTraRwht0wcGgQEX jpCXZ3j6ZCT8Q2NXuINujZ7UmZ4DUlqcCaf7GM+Ph2ofZ3u2MMMkkpgFwX4lCdPV 98OfiGvyEnj32HGQThrHmpPBNlxoqlat0YUfeiDtD4a+g29F1fdzxEhbHfi+E5Dq dX4JQmCFI6+z7YJa/OW42CUNzsyrINfdHnoVdeYSDXMobon4GVNS7Cc8ktiV/rPr rnetAKdeXTjlux5Bi6EASjqDqOY52TLJxltefTkCZZb3DKvAwKAlATpTdCxp2/Ey GTbtonT66FROHJer8cc+706dJKyfpcp/CJchlXNN1V8yhg== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-intermediate.pem ================================================ -----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c Nivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT FcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E 7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4 hSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp gawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g KQIDAQABo0EwPzAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTASBgNVHRMB Af8ECDAGAQH/AgEBMAsGA1UdDwQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAIdCQ NkReQcTU3aUCKqWdwCeFswg83lFHchgrH6QGSEwNI+y4YnEeSDs4KoU9ptLfVCoG WWYaTnePnzjTcOjYs0439/c2zS936EmVJs6EO55LK2YFKGHzhZARnAKVkiwUcHuZ bCrV9M3/muZmEwMXszzZfniREMyQZcfqbJjZZURRdmdK+dmHwHNDecXtbPNxQdB3 BRGtydZd5PH6ATR9zCz+ds3gRth+JFqzEYZH07BeHaCkRL80N7L8hH+E4+y0vVFJ sYfWMolbfPxEDRn6XQ/FROV3Kq2kSUIV09FBtE3b4aICB3ih78Xpzmvhh1g8rp7o oosP3AhvLCLjpruF/A== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-leaf-invalid-eku.pem ================================================ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c Nivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT FcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E 7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4 hSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp gawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g KQIDAQABo1AwTjAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB Af8EAjAAMAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG 9w0BAQsFAAOCAQEAAftcMInllQxRHV3t3jVJN68M6Bm7rOt761yx3p3DOlP4VG1d 9ZpNQkFLHxDWRFcChuWbEsVMXxGj7hvISKFgyrumlE10Yfn72SGMMRbHQrIGFHd/ bV6Xlr2IBqZzvCj1ZgoTb3o7U3I7xl2BP1+ScYhSigfCEh6b0U8TwCzaPE8Rfxik 8dHpW042MzAyiu0NbwH0iOVyj71Fx05/Hb/abgf3k1MV+0pAGC9cEkAyEORNXUda 1GcJ60hsfwMf7pUf+3f8OcEsznN7gVSWQn79L2nNN/Uh76Tel7JmoXwm5DgIF3Cg fLcj2PQo9MTXVuXkf9uJhgvkhmlElHfygYaYKA== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-leaf-invalid-ku.pem ================================================ -----BEGIN CERTIFICATE----- MIIDFzCCAf+gAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c Nivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT FcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E 7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4 hSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp gawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g KQIDAQABo08wTTAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB Af8EAjAAMAoGA1UdDwQDAwEAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3 DQEBCwUAA4IBAQADtd93gD08PW06DnaOYbvpBnEcNYEIS83N0vVLYFkm1UIbr5Ln QwORmsBMwOK04IhbnOMBt1Kb5CymFQkyhLHA2Y0KFnG6BYXKnVWgNYWb2yz6CShq KHZ6cu1vp/rADApv1IECZVKb5cZk7fCLk735SBuN8ybGdD3z2y6EINovq0c57GBN FY/4zPEHkBMlc+Ki/cv2ZwhQ+cAC/sU+vArjb5CWk8S3XfTaAsV30a41jZdmE30W yq4l67M4okuIouR6hxQal3TbsdfVVQUcAxmY1iFIXYmvEE48xHN4y4OjtBBTFM8h 99ePUcR4QrtPrvTmHIAgNupAZI4TUFamrimh -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-leaf-missing-eku.pem ================================================ -----BEGIN CERTIFICATE----- MIIDAzCCAeugAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c Nivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT FcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E 7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4 hSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp gawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g KQIDAQABozswOTAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB Af8EAjAAMAsGA1UdDwQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAAz58QW5XVZbg nzXKXhBYcbRRUqbw6edShLna8yzeB8acsuSwYz4sG4h41Q7opNBd3WhEn1dk6loo ZWM9NpG+t33LUgIjVsEUgt55kMB2DzBH7HHMsS7eGA7Qo/LX6tt3vX4bKG6HmHOI Gz7cPr8mRkO/EJHcJxTSRQ1uhQGXfjuBO5F2LSOsAUc8bP8VONJFk3lR/ZoON7qv +fGTYUp8qYlLQeANJHgywhTxWzcA0ew8j8+qDuTVsVxQUYqsA8m1TSHIPQXCo5gp YU7oyEtGt/ly6CDxVTEJVEZndP5roRhm3oYqCJIl9jDvPg7WTyxMtQ9boBzxVPix VRqHa9Q1tQ== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-leaf.pem ================================================ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c Nivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT FcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E 7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4 hSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp gawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g KQIDAQABo1AwTjAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB Af8EAjAAMAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG 9w0BAQsFAAOCAQEATrcFg1ZlHp95eTpHV0O5HP+XAbTh8OE55LkT1VYqDBd64THG jXvEwfDRiexWR5wkLltGJuHxmJq9avlSXBxObwbamSJu1YYiOumv1Bnxnf+wgrNY dz6KTeD18xNNASuDRIBoKoj6OF0wJigUMYJThXGCdke9fivMqV3JWtLzPM39cuu4 yV72EFBEp3/cVD5rIKK7Zfl4KW/ybpOMtvXCIT9GwnI/BgDuGjHimofwtncqzwRT cC8w1w9OIloadHOosOxRaT2PGcAWtNhL/clBj9Wwoc3hO5WCpwHGm50tbqGVGkxy uYljP7EnJ12llW2UIB2so60SCqOH5cNv8N60Jw== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-root-invalid-ku.pem ================================================ -----BEGIN CERTIFICATE----- MIIDBjCCAe6gAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9 iaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ kiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU tQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U lTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P bo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC RwIDAQABoz4wPDAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAPBgNVHRMB Af8EBTADAQH/MAsGA1UdDwQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAnPQAk1ng m/XwyfFe6uvz7PaNz6wryeWWDlEM4ON48Rrf/QhIsG3NU2/ftQUPv5CcOp7R9Xho qrC1YsJNypL/BPA4kdheDBp4HLmmX05FyXEG3l2WCGqC1/ZS3Ye4k9WmnsSCWL85 YImLXhBk9kwpYfPE2PMq5gqHVEKvRZGv+KzdHqt1LJKHUdgE/OtdFya8Af4N2BbB mRAPnC2jYIbApEVpM/kG9ANPZQteSGHsJSfWM2uZcbTpeNL55nVE1oXYGcXDbjUl AdYbgBiAk1mzmpGz2HMwLWhy5qK4d01dZvQOQZD+FjwqHNG2uHJQkP+qCeVOdRIC tdz2zF8ucjf3DA== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-root-missing-ku.pem ================================================ -----BEGIN CERTIFICATE----- MIIC+TCCAeGgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9 iaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ kiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU tQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U lTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P bo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC RwIDAQABozEwLzAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAPBgNVHRMB Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA038HUNVxomLhJ8zC1HQpR4fiY pMvxajYXW+h6wi4LS9TxWtxN86etDZWcc7BNYYqEtmn+TYdg3bpXW7uPMM0tpZ6f WUZ+yPGKJi6iyOpYHgJMIy7sbSMZHpkPUeMf9Ye8rILrmP8CfjxuT6cq9RpGDqXf +rltrXRzmTSecqEyjs9faxf57LE21+4Jpla3WA6fIzidKcMjbFQqqqUMu9OadXZO JZqFP18GThZToZs7pXKNlVvMwNNnCnyrn8WbL4j95IokNwzC7lI5opc+FOGUFEZh fnAByZ3AqmSFrcnE3+B5eSfupds4mcHnryqSP4/6xvd26aqs/qkmBJ+QkQO9 -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-root-noncritical-bc.pem ================================================ -----BEGIN CERTIFICATE----- MIIDAzCCAeugAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9 iaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ kiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU tQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U lTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P bo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC RwIDAQABozswOTAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRME BTADAQH/MAsGA1UdDwQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAOZonwl/4au3b /WKLy3OL0WEXbwhl6S4i9PxwtTmXSAO6GhPSLIfMrTQlLyay9L40aQ95dZvnfo2Z LOoMMpYOfo15YmtuyEAK7syh+UZTT78UNCqlkc0gSNUed6WucWHrAS90+TbFo/1/ mFGgYjao7GR755MOTFGlwa2eeYV+bEwGd9k1vTycQIOP4gLyBACP8KUMAlTEHnK2 0ZJ6GIpHVz5LalYCicxe0COKgKXER3l5YsnSLI5hvupeSld2jvNklCf94xMBOn/1 TsangKU8zZc4GmbPlb+OqxvXNOnMhCQlz3zj762eMsLI599vUA9g1tHAauf05ZN+ jQ9agtlATw== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/bogus-root.pem ================================================ -----BEGIN CERTIFICATE----- MIIDBjCCAe6gAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln c3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1 MDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9 iaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ kiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU tQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U lTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P bo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC RwIDAQABoz4wPDAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAPBgNVHRMB Af8EBTADAQH/MAsGA1UdDwQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAGxfbsceU WyuT59wIXqNjBd1HybiYEHZQw87o907ovdNZALLZ2URTPllIoNUiaxVa3VjG3ttj iVVDe1JLbS8/JaG+ZqQkHAorByM1tjxoIFiq+yaBYeg997etgFM1OVhbRNq744LE 2zPEeYTiokbQwDAeUtYRmo+9vK7gn7iNAb/pYOswMMtcGOZSj7ebvJQwkS5qDGMz zJ1pdkRpP0/kaLsZouaTsPiiJp3vV0QvVjOJKT765YF0pQCehl17JehDS6jgXhe5 gqatlDDm7ALG4bCGbqnC4XLYXaEstD4UbrUEvQ5lnO2+jbgDaOoyC6pzGjvC3p7u BX8EoFOjwFfx0w== -----END CERTIFICATE----- ================================================ FILE: test/assets/x509/build-testcases.py ================================================ #!/usr/bin/env python # 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. # build-testcases.py: generate some bogus X.509 testcases for sigstore's # unit tests. # # These testcases should already be checked-in; you can re-generate them # (with entirely new key material) using: # # python build-testcases.py # # ...while running from this directory. import datetime import os import sys from pathlib import Path from cryptography import x509 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.serialization import ( Encoding, load_pem_private_key, ) from cryptography.x509.oid import NameOID def _keypair(priv_key_file: Path): priv_key_bytes: bytes with priv_key_file.open("rb") as f: priv_key_bytes = f.read() priv_key = load_pem_private_key(priv_key_bytes, None) return priv_key.public_key(), priv_key _HERE = Path(__file__).resolve().parent _ROOT_PUBKEY, _ROOT_PRIVKEY = _keypair(_HERE / "root-privkey.pem") _NONROOT_PUBKEY, _ = _keypair(_HERE / "nonroot-privkey.pem") _NOT_VALID_BEFORE_DATE = datetime.datetime(2023, 1, 1) _A_VERY_LONG_TIME = datetime.timedelta(days=365 * 1000) def _builder() -> x509.CertificateBuilder: builder = x509.CertificateBuilder() builder = builder.subject_name( x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, "sigstore-python-bogus-cert"), ] ) ) builder = builder.issuer_name( x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, "sigstore-python-bogus-cert"), ] ) ) builder = builder.not_valid_before(_NOT_VALID_BEFORE_DATE) builder = builder.not_valid_after(_NOT_VALID_BEFORE_DATE + _A_VERY_LONG_TIME) builder = builder.serial_number(666) builder = builder.add_extension( x509.SubjectAlternativeName([x509.DNSName("bogus.example.com")]), critical=False ) return builder def _finalize( builder: x509.CertificateBuilder, *, pubkey=_ROOT_PUBKEY, privkey=_ROOT_PRIVKEY ) -> x509.Certificate: builder = builder.public_key(pubkey) return builder.sign(private_key=privkey, algorithm=hashes.SHA256()) def _dump(cert: x509.Certificate, filename: Path): pem = cert.public_bytes(Encoding.PEM) if not filename.exists() or os.getenv("TESTCASE_OVERWRITE"): print(f"[+] writing: {filename}", file=sys.stderr) filename.write_bytes(pem) else: print(f"[+] skipping: {filename}", file=sys.stderr) def bogus_root() -> x509.Certificate: """ A valid root CA certificate. """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) return _finalize(builder) def bogus_root_noncritical_bc() -> x509.Certificate: """ An invalid root CA certificate, due to the BasicConstraints extension being marked as non-critical. """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=False, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) return _finalize(builder) def bogus_root_missing_ku() -> x509.Certificate: """ An invalid root CA certificate, due to a missing KeyUsage extension. """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True, ) return _finalize(builder) def bogus_root_invalid_ku() -> x509.Certificate: """ An invalid root CA certificate, due to inconsistent KeyUsage state (KU.keyCertSign <> BC.ca) """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=False, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) return _finalize(builder) def bogus_intermediate() -> x509.Certificate: """ A valid intermediate CA certificate, for Sigstore purposes. """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=1), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) return _finalize(builder, pubkey=_NONROOT_PUBKEY) def bogus_intermediate_with_eku() -> x509.Certificate: """ A valid intermediate CA certificate, for Sigstore purposes. This is like `bogus_intermediate`, except that it also contains a code signing EKU entitlement to make sure we don't treat that as an incorrect signal. """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=1), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) builder = builder.add_extension( x509.ExtendedKeyUsage(usages=[x509.OID_CODE_SIGNING]), critical=False, ) return _finalize(builder, pubkey=_NONROOT_PUBKEY) def bogus_leaf() -> x509.Certificate: """ A valid leaf certificate, for Sigstore purposes. """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=False, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) builder = builder.add_extension( x509.ExtendedKeyUsage(usages=[x509.OID_CODE_SIGNING]), critical=False, ) return _finalize(builder, pubkey=_NONROOT_PUBKEY) def bogus_leaf_invalid_ku() -> x509.Certificate: """ An invalid leaf certificate (for Sigstore purposes), due to an invalid KeyUsage (lacking the digitalSignature entitlement). """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=False, key_cert_sign=False, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) builder = builder.add_extension( x509.ExtendedKeyUsage(usages=[x509.OID_CODE_SIGNING]), critical=False, ) return _finalize(builder, pubkey=_NONROOT_PUBKEY) def bogus_leaf_invalid_eku() -> x509.Certificate: """ An invalid leaf certificate (for Sigstore purposes), due to an invalid ExtendedKeyUsage (lacking the code signing entitlement). """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=False, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) builder = builder.add_extension( x509.ExtendedKeyUsage(usages=[x509.OID_SERVER_AUTH]), critical=False, ) return _finalize(builder, pubkey=_NONROOT_PUBKEY) def bogus_leaf_missing_eku() -> x509.Certificate: """ An invalid leaf certificate (for Sigstore purposes), due to a missing ExtendedKeyUsage extension. """ builder = _builder() builder = builder.add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, ) builder = builder.add_extension( x509.KeyUsage( digital_signature=True, key_cert_sign=False, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, crl_sign=False, encipher_only=False, decipher_only=False, ), critical=False, ) return _finalize(builder, pubkey=_NONROOT_PUBKEY) # Individual testcases; see each function's docstring. _dump(bogus_root(), _HERE / "bogus-root.pem") _dump(bogus_root_noncritical_bc(), _HERE / "bogus-root-noncritical-bc.pem") _dump(bogus_root_missing_ku(), _HERE / "bogus-root-missing-ku.pem") _dump(bogus_root_invalid_ku(), _HERE / "bogus-root-invalid-ku.pem") _dump(bogus_intermediate(), _HERE / "bogus-intermediate.pem") _dump(bogus_intermediate_with_eku(), _HERE / "bogus-intermediate-with-eku.pem") _dump(bogus_leaf(), _HERE / "bogus-leaf.pem") _dump(bogus_leaf_invalid_ku(), _HERE / "bogus-leaf-invalid-ku.pem") _dump(bogus_leaf_invalid_eku(), _HERE / "bogus-leaf-invalid-eku.pem") _dump(bogus_leaf_missing_eku(), _HERE / "bogus-leaf-missing-eku.pem") ================================================ FILE: test/assets/x509/nonroot-privkey.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC402QfBePPiHp0 bYH6D9w2K+z1lWHQc1YoSPEd1JplDYJdWOcf3WsSPslOwRktiwgK9aOWVgsMJ86Z XyV/A5MVw8BSrKxoDtIXVsf/wqpMB9piu+tVBowXE7E59yWYwFzZw3Ya1ysstmud m7BnjQTtlyHHm4JnueO0pXYkvRxAimOz6uc1XGrLXnz9B/8Bx4Llz/mEjRYj2bD2 4vufMbiFKb8Ja93t+9ALpRX9C30Giy15UhUH/ehh+y2fCGv/xqma+rTPr2I5/3/B QY9spamBrAXSX7ZnzEx+8Hbl3j2tdYi3LC14GWQ9WIT57DYjHtQBBnnkQ9nOGEjs +jOi7+ApAgMBAAECggEAN7zoUMLB9PA/naT4saTe0CdnCpjGKsrdjMCSlmBrP1ZX njcVXHK1u4bbxrhNE4L+Je/2KXxBUKUglPgwoqE9Vi72bPhN9gOiMA+nuOXH3a3w mh351mZnEP6LT+PMnshEOBfOIkIJby6EPb+Z72CDv/L36O5o4UcZ+Hx9qI6vWnbe DhRpVuyp2Zbw6XOBSu5MWIpWjSBaJmaV2D5ad/EDC/XmRG+gMDHD1G3Hj0Mjlq1F n4G94UJwqWGwaaQGjE6gKino/ecbEmaOl4KGKrcdU0wZodtxdN8q54qthQ4z48iu PTWQQSPgUm9OjnWh/Qg3KKQxuY7Hyfn9Lf0TywPj0QKBgQDqxJA2pj8542foJQyj PaF29awaKwYpK7IFpLbUAer7kw9P0UWFiOpSQWLHecW0J0YyQjOynS0/FbyCgcNF 4JxoVaBJGKyR1bGuMVD/u12RgfXktyLW+e3jEPoBqoIuK5AFMamT+nxT0WBjRJ0c oVdsFDol5pXUfKpP7btpTIiUgwKBgQDJioyXcGpuiE8lfvbrQI2TO4rv4m1J1Nyk pwl1MrUBesT3+JrB4pT9AqknBN6koenknY7ZVlhvHbAPbgnSi7HW1xJcsSew7Dxl qgnFj26kEMptZjHlzELTPr34RCvP23iUfb2yiEj0kbFYMOBsu7oxC6+lT+XbIkIZ bmc7Y4QQ4wKBgQCj3lpPWxGM5ZeUqa+9jfpTX74mcduWB0L2v3dCWqhbu9WXQBrH z77HdY5ucCg4zKUp1Z3iUeXQP+raKZtU/igOh54fB5MFJGUmkpPYPT9dnpo1cENo TQHoWeQ4H31Inu2jQnv8p336v44JHE6SOmgcL6464E27CN2Udvs2z84R4wKBgCfh KoCs1eKZRk/9F47lbx47Ifrlqwp4/E/4XX67UeXBDUikALtswl5uMFpwND4Pa+C4 7JNE6qrSDQyAkaD/02jXleKRi3EOzcSwKM7W2uXMDMIo/qaiDHcQaza9Bo5St0Fq wCaboRQD4Du7MC1T2DvsPA1SCgGafcnadsLhpjhRAoGBAM2ofL3gyu9/hSzmlcCf nhnrE3ccqH5ln9//LnuzMsaqFWFG9zhNrUYuujkzxeaF9P+0sXfLjHnC9yNlO2qa oyoqySXDy0Kc0q7iVF9XHrv0KMt4KO0KQ6XH3SfM3+0SssSMwyG0M09H5Lh+3GP7 QTjwn+hqS4k6vFLZ4HHy+qQi -----END PRIVATE KEY----- ================================================ FILE: test/assets/x509/root-privkey.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJGnUWsZb/91FD wpxo4z2JoPZVQnDj8fnXcm7EL/tpD7tY98EtDjAvK2gkf0Tt5JOsA+OHuWKVthW8 vZMUsImSJoa46C86kI3y7EL2J7+v66L5iB7EnCUyPDF+rHrtZNdkm0L8ZVLjkhJ+ royQjBS1Ba9M9cmceXZ6khePh6XpchBZAv+SgGBSXNs2t93BnU2kCbSM0MrjgONc MLATLxSVMMyV7V0BvhXzQaUXmzztxq42nmx+CHPTmKpmJNKDjjrW/8QUftMSUC25 ouprfo9ujU4QiR5bJ6PyfrSYVvBja/gWtv0b29AxjWBXu8u1618mmAooiP+Ir+Nu yuuGOwJHAgMBAAECggEAFZogdWbFNCCycNzA+r6yN7b7zwPF5vkcConHHo7oQDcJ kRB9W+QixpEF7P8OKJ8S7SQ2duKx6tK2OgyDy0dSt8l9/kh+o5lZ43wqjeY41Tey zYAoN0bDSMamKxfG//otmFKEmw0dSXHBnSxjJWHOwEqTM+lRFgcGyZAo32jwUweW D8cjuYmoL87I6xMmmcAnSjhTv51inboAoo/PK1OHVX7rcmZ5N61/zEF2swo+wzsm XkgVQcGa6tjstT85/c6KFx8yZju/Na8jqzpTJS1o11QUy86t9VHF8qNyOrzFrTQr EBP2kp5QCrCsyy4yk9n/bdKY6BR8h1lhS6yNCvBHSQKBgQDnvtUVwJLy5fvbbexc f7+7kIVPAGEJAfb5ZYsg82sECKCEyg4TSMxzWPw30XVrdC9+tqYRGk4AO77hdEz9 YUCs2KSQU22Ve50oqRP+I2vkTQWbdmDW+saB0+HHjaPG5Dwt/pdPJwlloqUgHSXr J2KOAjzs4qsirGb8LN4LDiM0nwKBgQDeJqFBYRlJd1mSlLArrVHFmm/0dQTeHS2h xYOll6iSxPrd6++9FuCsCTdAnLpA0V8gRY7z/jKWY8CQbAYtLvBAz2Sn+kaHXNSn /pzWRl4sMSnfa16GZWz46m8NBhTdUGdA94Wj3LqDTFDiXwwYRwuPtewK820ZObh5 +vD6Z0fpWQKBgQCMMh84lJKRjV5LBfnqj4IPV0O+Yk1RpLWjdLGxUnEYNJvfGVlg gzbkRR34KqftRJGDB735NL+hVoOIYtI8qvv0VO9hPIdb2jdeJMMqiIU5zPqqbPfy ti0m12aMUXyV0vcxIAarZMNDkBxzDA8nbmEp5eKzsAC17jQzNHVznK7howKBgBco j8bxCGHQP1Y4ieUDvHKNFv609Dzzbb5fiMnKdZhXUI+x+NwNdn54t3nU3NXE/dWv aqek6EElRP3JRRuQuRsIg8W/IXsbAlBBCriLvWV9+o9/8eqwyBtq1QjWiXZI23q6 UwQyDn+BhS0UG36saVgh7ul1Vvo6OjD9KAHyolyBAoGAIgncKn3cytyeub8WYzPh OU8DlAuiwMylMrOtBASDSgnx3zVefyGUqSXh8wK92MgZAiBYI4PaHymbil3PMC0A 1PT8f2Cen70L2aYMDt4+8c5arT6v8bGwtdz/BKjZTca3nblU4nnhhd2aY5e3Ut3d ffkc8EfGnIeo7hZBqKAe0gY= -----END PRIVATE KEY----- ================================================ FILE: test/conftest.py ================================================ # Copyright 2024 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. import os from pathlib import Path import pytest from id import ( AmbientCredentialError, GitHubOidcPermissionCredentialError, detect_credential, ) _ASSETS = (Path(__file__).parent / "assets").resolve() assert _ASSETS.is_dir() TEST_CLIENT_ID = "sigstore" @pytest.fixture def asset(): def _asset(name: str) -> Path: return _ASSETS / name return _asset def _has_oidc_id(): # If there are tokens manually defined for us in the environment, use them. if os.getenv("SIGSTORE_IDENTITY_TOKEN_production") or os.getenv( "SIGSTORE_IDENTITY_TOKEN_staging" ): return True try: token = detect_credential(TEST_CLIENT_ID) if token is None: return False except GitHubOidcPermissionCredentialError: # On GitHub Actions, forks do not have access to OIDC identities. # We differentiate this case from other GitHub credential errors, # since it's a case where we want to skip (i.e. return False). # # We also skip when the repo isn't our own, since downstream # regression testers (e.g. PyCA Cryptography) don't necessarily # want to give our unit tests access to an OIDC identity. return ( os.getenv("GITHUB_REPOSITORY") == "sigstore/sigstore-python" and os.getenv("GITHUB_EVENT_NAME") != "pull_request" ) except AmbientCredentialError: # If ambient credential detection raises, then we *are* in an ambient # environment but one that's been configured incorrectly. We # pass this through, so that the CI fails appropriately rather than # silently skipping the faulty tests. return True return True def _has_timestamp_authority_configured() -> bool: """ Check if there is a Timestamp Authority that has been configured """ return os.getenv("TEST_SIGSTORE_TIMESTAMP_AUTHORITY_URL") is not None def pytest_addoption(parser): parser.addoption( "--skip-online", action="store_true", help="skip tests that require network connectivity", ) parser.addoption( "--skip-staging", action="store_true", help="skip tests that require Sigstore staging infrastructure", ) def pytest_runtest_setup(item): # Do we need a network connection? online = False for mark in ["online", "staging", "production"]: if mark in item.keywords: online = True if online and item.config.getoption("--skip-online"): pytest.skip( "skipping test that requires network connectivity due to `--skip-online` flag" ) elif "ambient_oidc" in item.keywords and not _has_oidc_id(): pytest.skip("skipping test that requires an ambient OIDC credential") if "staging" in item.keywords and item.config.getoption("--skip-staging"): pytest.skip( "skipping test that requires staging infrastructure due to `--skip-staging` flag" ) if ( "timestamp_authority" in item.keywords and not _has_timestamp_authority_configured() ): pytest.skip("skipping test that requires a Timestamp Authority") def pytest_configure(config): config.addinivalue_line( "markers", "staging: mark test as requiring Sigstore staging infrastructure" ) config.addinivalue_line( "markers", "production: mark test as requiring Sigstore production infrastructure", ) config.addinivalue_line( "markers", "online: mark test as requiring network connectivity (but not a specific Sigstore infrastructure)", ) config.addinivalue_line( "markers", "ambient_oidc: mark test as requiring an ambient OIDC identity" ) config.addinivalue_line( "markers", "timestamp_authority: mark test as requiring a timestamp authority" ) ================================================ FILE: test/integration/cli/__init__.py ================================================ # 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. ================================================ FILE: test/integration/cli/conftest.py ================================================ # 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. from collections.abc import Callable from pathlib import Path import pytest from sigstore._cli import main @pytest.fixture def asset_integration(asset): def _asset(name: str) -> Path: return asset(f"integration/{name}") return _asset @pytest.fixture(scope="function") def sigstore() -> Callable: def _sigstore(*args: str): main(list(args)) return _sigstore ================================================ FILE: test/integration/cli/test_attest.py ================================================ # Copyright 2024 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. from pathlib import Path import pytest from sigstore.dsse._predicate import PredicateType from sigstore.models import Bundle from sigstore.verify import Verifier from sigstore.verify.policy import UnsafeNoOp def get_cli_params( pred_type: str, pred_path: Path, artifact_path: Path, overwrite: bool = False, bundle_path: Path | None = None, ) -> list[str]: cli_params = [ "--staging", "attest", "--predicate-type", pred_type, "--predicate", str(pred_path), ] if bundle_path is not None: cli_params.extend(["--bundle", str(bundle_path)]) if overwrite: cli_params.append("--overwrite") cli_params.append(str(artifact_path)) return cli_params @pytest.mark.staging @pytest.mark.ambient_oidc @pytest.mark.parametrize( ("predicate_type", "predicate_filename"), [ (PredicateType.SLSA_v0_2, "slsa_predicate_v0_2.json"), (PredicateType.SLSA_v1_0, "slsa_predicate_v1_0.json"), ], ) def test_attest_success_default_output_bundle( capsys, sigstore, asset_integration, predicate_type, predicate_filename ): predicate_path = asset_integration(f"attest/{predicate_filename}") artifact = asset_integration("a.txt") expected_output_bundle = artifact.with_name("a.txt.sigstore.json") assert not expected_output_bundle.exists() sigstore( *get_cli_params( pred_type=predicate_type, pred_path=predicate_path, artifact_path=artifact, ) ) assert expected_output_bundle.exists() verifier = Verifier.staging() with open(expected_output_bundle, "r", encoding="utf-8") as bundle_file: bundle = Bundle.from_json(bundle_file.read()) verifier.verify_dsse(bundle=bundle, policy=UnsafeNoOp()) expected_output_bundle.unlink() captures = capsys.readouterr() assert captures.out.endswith( f"Sigstore bundle written to {expected_output_bundle}\n" ) @pytest.mark.staging @pytest.mark.ambient_oidc def test_attest_success_custom_output_bundle( capsys, sigstore, asset_integration, tmp_path ): predicate_type = PredicateType.SLSA_v0_2 predicate_filename = "slsa_predicate_v0_2.json" predicate_path = asset_integration(f"attest/{predicate_filename}") artifact = asset_integration("a.txt") output_bundle = tmp_path / "bundle.json" assert not output_bundle.exists() sigstore( *get_cli_params( pred_type=predicate_type, pred_path=predicate_path, artifact_path=artifact, bundle_path=output_bundle, ) ) assert output_bundle.exists() captures = capsys.readouterr() assert captures.out.endswith(f"Sigstore bundle written to {output_bundle}\n") @pytest.mark.staging @pytest.mark.ambient_oidc def test_attest_overwrite_existing_bundle( capsys, sigstore, asset_integration, tmp_path ): predicate_type = PredicateType.SLSA_v0_2 predicate_filename = "slsa_predicate_v0_2.json" predicate_path = asset_integration(f"attest/{predicate_filename}") artifact = asset_integration("a.txt") output_bundle = tmp_path / "bundle.json" assert not output_bundle.exists() cli_params = get_cli_params( pred_type=predicate_type, pred_path=predicate_path, artifact_path=artifact, bundle_path=output_bundle, ) sigstore(*cli_params) assert output_bundle.exists() # On invalid argument errors we call `Argumentparser.error`, which prints # a message and exits with code 2 with pytest.raises(SystemExit) as e: sigstore(*cli_params) assert e.value.code == 2 assert output_bundle.exists() captures = capsys.readouterr() assert captures.err.endswith( f"Refusing to overwrite outputs without --overwrite: {output_bundle}\n" ) cli_params.append("--overwrite") sigstore(*cli_params) assert output_bundle.exists() assert captures.out.endswith(f"Sigstore bundle written to {output_bundle}\n") def test_attest_invalid_predicate_type(capsys, sigstore, asset_integration, tmp_path): predicate_type = "invalid_type" predicate_filename = "slsa_predicate_v0_2.json" predicate_path = asset_integration(f"attest/{predicate_filename}") artifact = asset_integration("a.txt") output_bundle = tmp_path / "bundle.json" # On invalid argument errors we call `Argumentparser.error`, which prints # a message and exits with code 2 with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( pred_type=predicate_type, pred_path=predicate_path, artifact_path=artifact, bundle_path=output_bundle, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith(f"invalid PredicateType value: '{predicate_type}'\n") def test_attest_mismatching_predicate(capsys, sigstore, asset_integration, tmp_path): predicate_type = PredicateType.SLSA_v0_2 predicate_filename = "slsa_predicate_v1_0.json" predicate_path = asset_integration(f"attest/{predicate_filename}") artifact = asset_integration("a.txt") output_bundle = tmp_path / "bundle.json" # On invalid argument errors we call `Argumentparser.error`, which prints # a message and exits with code 2 with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( pred_type=predicate_type, pred_path=predicate_path, artifact_path=artifact, bundle_path=output_bundle, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert f'Unable to parse predicate of type "{predicate_type}":' in captures.err def test_attest_missing_predicate(capsys, sigstore, asset_integration, tmp_path): predicate_type = PredicateType.SLSA_v0_2 predicate_filename = "doesnt_exist.json" predicate_path = asset_integration(f"attest/{predicate_filename}") artifact = asset_integration("a.txt") output_bundle = tmp_path / "bundle.json" # On invalid argument errors we call `Argumentparser.error`, which prints # a message and exits with code 2 with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( pred_type=predicate_type, pred_path=predicate_path, artifact_path=artifact, bundle_path=output_bundle, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith(f"Predicate must be a file: {predicate_path}\n") def test_attest_invalid_json_predicate(capsys, sigstore, asset_integration, tmp_path): predicate_type = PredicateType.SLSA_v0_2 predicate_path = asset_integration("a.txt") artifact = asset_integration("a.txt") output_bundle = tmp_path / "bundle.json" # On invalid argument errors we call `Argumentparser.error`, which prints # a message and exits with code 2 with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( pred_type=predicate_type, pred_path=predicate_path, artifact_path=artifact, bundle_path=output_bundle, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert f'Unable to parse predicate of type "{predicate_type}":' in captures.err ================================================ FILE: test/integration/cli/test_plumbing.py ================================================ # 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. import pytest from sigstore_models.common.v1 import HashAlgorithm from sigstore.hashes import Hashed from sigstore.models import Bundle, InvalidBundle from sigstore.verify import policy from sigstore.verify.verifier import Verifier def test_fix_bundle_fixes_missing_checkpoint(capsys, sigstore, asset_integration): invalid_bundle = asset_integration("Python-3.12.5.tgz.sigstore") # The bundle is invalid, because it's missing a checkpoint # for its inclusion proof. with pytest.raises( InvalidBundle, match="entry must contain inclusion proof, with checkpoint" ): Bundle.from_json(invalid_bundle.read_text()) # Running `sigstore plumbing fix-bundle` emits a fixed bundle. sigstore("plumbing", "fix-bundle", "--bundle", str(invalid_bundle)) captures = capsys.readouterr() # The bundle now loads correctly. bundle = Bundle.from_json(captures.out) # We didn't pass `--upgrade-version` so the version is still v0.1. assert bundle._inner.media_type == Bundle.BundleType.BUNDLE_0_1 # ...and the fixed bundle can now be used to verify the `Python-3.12.5.tgz` # release. verifier = Verifier.production() verifier.verify_artifact( Hashed( algorithm=HashAlgorithm.SHA2_256, digest=bytes.fromhex( "38dc4e2c261d49c661196066edbfb70fdb16be4a79cc8220c224dfeb5636d405" ), ), bundle, policy.AllOf( [ policy.Identity( identity="thomas@python.org", issuer="https://accounts.google.com" ) ] ), ) def test_fix_bundle_upgrades_bundle(capsys, sigstore, asset_integration): invalid_bundle = asset_integration("Python-3.12.5.tgz.sigstore") # Running `sigstore plumbing fix-bundle --upgrade-version` # emits a fixed bundle. sigstore( "plumbing", "fix-bundle", "--upgrade-version", "--bundle", str(invalid_bundle) ) captures = capsys.readouterr() # The bundle now loads correctly. bundle = Bundle.from_json(captures.out) # The bundle is now the latest version (v0.3). assert bundle._inner.media_type == Bundle.BundleType.BUNDLE_0_3 # ...and the upgraded (and fixed) bundle can still verify # the release. # ...and the fixed can now be used to verify the `Python-3.12.5.tgz` release. verifier = Verifier.production() verifier.verify_artifact( Hashed( algorithm=HashAlgorithm.SHA2_256, digest=bytes.fromhex( "38dc4e2c261d49c661196066edbfb70fdb16be4a79cc8220c224dfeb5636d405" ), ), bundle, policy.AllOf( [ policy.Identity( identity="thomas@python.org", issuer="https://accounts.google.com" ) ] ), ) ================================================ FILE: test/integration/cli/test_sign.py ================================================ # Copyright 2024 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. from pathlib import Path import pytest from sigstore.models import Bundle from sigstore.verify import Verifier from sigstore.verify.policy import UnsafeNoOp def get_cli_params( artifact_paths: list[Path], overwrite: bool = False, no_default_files: bool = False, output_directory: Path | None = None, bundle_path: Path | None = None, signature_path: Path | None = None, certificate_path: Path | None = None, trust_config_path: Path | None = None, ) -> list[str]: if trust_config_path is not None: cli_params = ["--trust-config", str(trust_config_path), "sign"] else: cli_params = ["--staging", "sign"] if output_directory is not None: cli_params.extend(["--output-directory", str(output_directory)]) if bundle_path is not None: cli_params.extend(["--bundle", str(bundle_path)]) if signature_path is not None: cli_params.extend(["--signature", str(signature_path)]) if certificate_path is not None: cli_params.extend(["--certificate", str(certificate_path)]) if overwrite: cli_params.append("--overwrite") if no_default_files: cli_params.append("--no-default-files") cli_params.extend([str(p) for p in artifact_paths]) return cli_params @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_success_default_output_bundle( capsys, sigstore, asset_integration, tmp_path ): artifact = asset_integration("a.txt") expected_output_bundle = tmp_path / "a.txt.sigstore.json" sigstore( *get_cli_params( artifact_paths=[artifact], output_directory=tmp_path, ) ) assert expected_output_bundle.exists() verifier = Verifier.staging() with ( open(expected_output_bundle, "r", encoding="utf-8") as bundle_file, open(artifact, "rb") as input_file, ): bundle = Bundle.from_json(bundle_file.read()) verifier.verify_artifact( input_=input_file.read(), bundle=bundle, policy=UnsafeNoOp() ) captures = capsys.readouterr() assert captures.out.endswith( f"Sigstore bundle written to {expected_output_bundle}\n" ) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_success_multiple_artifacts(capsys, sigstore, asset_integration, tmp_path): artifacts: list[Path] = [ asset_integration("a.txt"), asset_integration("b.txt"), asset_integration("c.txt"), ] sigstore( *get_cli_params( artifact_paths=artifacts, output_directory=tmp_path, ) ) captures = capsys.readouterr() for artifact in artifacts: expected_output_bundle = tmp_path / f"{artifact.name}.sigstore.json" assert f"Sigstore bundle written to {expected_output_bundle}\n" in captures.out assert expected_output_bundle.exists() verifier = Verifier.staging() with ( open(expected_output_bundle, "r", encoding="utf-8") as bundle_file, open(artifact, "rb") as input_file, ): bundle = Bundle.from_json(bundle_file.read()) verifier.verify_artifact( input_=input_file.read(), bundle=bundle, policy=UnsafeNoOp() ) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_success_multiple_artifacts_rekor_v2( capsys, sigstore, asset_integration, asset, tmp_path ): """This is a copy of test_sign_success_multiple_artifacts that exists to ensure the multi-threaded signing works with rekor v2 as well: this test can be removed when v2 is the default """ artifacts: list[Path] = [ asset_integration("a.txt"), asset_integration("b.txt"), asset_integration("c.txt"), ] sigstore( *get_cli_params( artifact_paths=artifacts, output_directory=tmp_path, ) ) captures = capsys.readouterr() for artifact in artifacts: expected_output_bundle = tmp_path / f"{artifact.name}.sigstore.json" assert f"Sigstore bundle written to {expected_output_bundle}\n" in captures.out assert expected_output_bundle.exists() verifier = Verifier.staging() with ( open(expected_output_bundle, "r", encoding="utf-8") as bundle_file, open(artifact, "rb") as input_file, ): bundle = Bundle.from_json(bundle_file.read()) verifier.verify_artifact( input_=input_file.read(), bundle=bundle, policy=UnsafeNoOp() ) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_success_custom_outputs(capsys, sigstore, asset_integration, tmp_path): artifact = asset_integration("a.txt") output_bundle = tmp_path / "bundle.json" output_cert = tmp_path / "cert.cert" output_signature = tmp_path / "signature.sig" sigstore( *get_cli_params( artifact_paths=[artifact], bundle_path=output_bundle, certificate_path=output_cert, signature_path=output_signature, ) ) assert output_bundle.exists() assert output_cert.exists() assert output_signature.exists() captures = capsys.readouterr() assert captures.out.endswith( f"Signature written to {output_signature}\nCertificate written to {output_cert}\nSigstore bundle written to {output_bundle}\n" ) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_success_custom_output_dir(capsys, sigstore, asset_integration, tmp_path): artifact = asset_integration("a.txt") expected_output_bundle = tmp_path / "a.txt.sigstore.json" sigstore( *get_cli_params( artifact_paths=[artifact], output_directory=tmp_path, ) ) assert expected_output_bundle.exists() captures = capsys.readouterr() assert captures.out.endswith( f"Sigstore bundle written to {expected_output_bundle}\n" ) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_success_no_default_files(capsys, sigstore, asset_integration, tmp_path): artifact = asset_integration("a.txt") default_output_bundle = tmp_path / "a.txt.sigstore.json" output_cert = tmp_path / "cert.cert" output_signature = tmp_path / "sig.sig" sigstore( *get_cli_params( artifact_paths=[artifact], signature_path=output_signature, certificate_path=output_cert, no_default_files=True, ) ) assert output_cert.exists() assert output_signature.exists() assert not default_output_bundle.exists() captures = capsys.readouterr() assert captures.out.endswith( f"Signature written to {output_signature}\nCertificate written to {output_cert}\n" ) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_overwrite_existing_bundle(capsys, sigstore, asset_integration, tmp_path): artifact = asset_integration("a.txt") expected_output_bundle = tmp_path / "a.txt.sigstore.json" sigstore( *get_cli_params( artifact_paths=[artifact], output_directory=tmp_path, ) ) assert expected_output_bundle.exists() sigstore( *get_cli_params( artifact_paths=[artifact], output_directory=tmp_path, overwrite=True, ) ) assert expected_output_bundle.exists() with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact], output_directory=tmp_path, overwrite=False, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( f"Refusing to overwrite outputs without --overwrite: {expected_output_bundle}\n" ) def test_sign_fails_with_default_files_and_bundle_options( capsys, sigstore, asset_integration ): artifact = asset_integration("a.txt") output_bundle = artifact.with_name("a.txt.sigstore.json") with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact], bundle_path=output_bundle, no_default_files=True, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "--no-default-files may not be combined with --bundle.\n" ) def test_sign_fails_with_multiple_inputs_and_custom_output( capsys, sigstore, asset_integration ): artifact = asset_integration("a.txt") with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact, artifact], bundle_path=artifact.with_name("a.txt.sigstore.json"), ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\n" ) with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact, artifact], certificate_path=artifact.with_name("a.txt.cert"), ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\n" ) with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact, artifact], signature_path=artifact.with_name("a.txt.sig"), ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\n" ) def test_sign_fails_with_output_dir_and_custom_output_files( capsys, sigstore, asset_integration ): artifact = asset_integration("a.txt") with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact], bundle_path=artifact.with_name("a.txt.sigstore.json"), output_directory=artifact.parent, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\n" ) with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact], certificate_path=artifact.with_name("a.txt.cert"), output_directory=artifact.parent, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\n" ) with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact], signature_path=artifact.with_name("a.txt.sig"), output_directory=artifact.parent, ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\n" ) def test_sign_fails_without_both_output_cert_and_signature( capsys, sigstore, asset_integration ): artifact = asset_integration("a.txt") with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact], certificate_path=artifact.with_name("a.txt.cert"), ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature and --certificate must be used together.\n" ) with pytest.raises(SystemExit) as e: sigstore( *get_cli_params( artifact_paths=[artifact], signature_path=artifact.with_name("a.txt.sig"), ) ) assert e.value.code == 2 captures = capsys.readouterr() assert captures.err.endswith( "Error: --signature and --certificate must be used together.\n" ) ================================================ FILE: test/integration/cli/test_verify.py ================================================ # Copyright 2024 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. import pytest @pytest.mark.staging def test_regression_verify_legacy_bundle(capsys, caplog, asset_integration, sigstore): # Check that verification continues to work when legacy bundle is present (*.sigstore) and # no cert, sig and normal bundle (*.sigstore.json) are present. artifact_filename = "bundle_v3.txt" artifact = asset_integration(artifact_filename) legacy_bundle = asset_integration(f"{artifact_filename}.sigstore") sig = asset_integration(f"{artifact_filename}.sig") cert = asset_integration(f"{artifact_filename}.crt") bundle = asset_integration(f"{artifact_filename}.sigstore.json") assert not cert.is_file() assert not sig.is_file() assert not bundle.is_file() sigstore( "--staging", "verify", "identity", str(artifact), "--cert-identity", "william@yossarian.net", "--cert-oidc-issuer", "https://github.com/login/oauth", ) captures = capsys.readouterr() assert captures.err == f"OK: {artifact.absolute()}\n" assert len(caplog.records) == 1 assert ( caplog.records[0].message == f"{artifact.absolute()}: {legacy_bundle.absolute()} should be named {bundle.absolute()}. Support for discovering 'bare' .sigstore inputs will be deprecated in a future release." ) ================================================ FILE: test/integration/sigstore-python-conformance ================================================ #!/usr/bin/env python3 """ A wrapper to convert `sigstore-conformance` CLI protocol invocations to match `sigstore-python`. """ import json import os import sys from contextlib import suppress from pathlib import Path from tempfile import NamedTemporaryFile # The signing config in this trust_config is not used: it's just here # so the built trustconfig is complete trust_config = { "mediaType": "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json", "signingConfig": { "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", "caUrls": [{ "url": "https://fulcio.example.com", "majorApiVersion": 1, "operator": "", "validFor": {"start": "1970-01-01T01:01:01Z"} }], "oidcUrls": [], "rekorTlogUrls": [{ "url": "https://rekor.example.com", "majorApiVersion": 1, "operator": "", "validFor": {"start": "1970-01-01T01:01:01Z"} }], "tsaUrls": [], "rekorTlogConfig": {"selector": "ANY"}, "tsaConfig": {"selector": "ANY"}, }, } SUBCMD_REPLACEMENTS = { "sign-bundle": "sign", "verify-bundle": "verify", } ARG_REPLACEMENTS = { "--certificate-identity": "--cert-identity", "--certificate-oidc-issuer": "--cert-oidc-issuer", } # Trim the script name. fixed_args = sys.argv[1:] # Substitute incompatible subcommands. subcmd = fixed_args[0] if subcmd in SUBCMD_REPLACEMENTS: fixed_args[0] = SUBCMD_REPLACEMENTS[subcmd] # Build base command with optional staging argument command = ["sigstore"] if "--staging" in fixed_args: command.append("--staging") fixed_args.remove("--staging") # We may get "--trusted-root" and "--signing-config" as argument but sigstore-python # wants "--trust-config": trusted_root_path = None with suppress(ValueError): i = fixed_args.index("--trusted-root") trusted_root_path = fixed_args[i + 1] fixed_args.pop(i) fixed_args.pop(i) signing_config_path = None with suppress(ValueError): i = fixed_args.index("--signing-config") signing_config_path = fixed_args[i + 1] fixed_args.pop(i) fixed_args.pop(i) # If we did get a trustedroot, write a matching trustconfig into a temp file # Use given signingconfig if possible, otherwise use the fake one in template with NamedTemporaryFile(mode="wt") as temp_file: if trusted_root_path is not None: with open(trusted_root_path) as f: trusted_root = json.load(f) trust_config["trustedRoot"] = trusted_root if signing_config_path is not None: with open(signing_config_path) as f: signing_config = json.load(f) trust_config["signingConfig"] = signing_config json.dump(trust_config, temp_file) temp_file.flush() command.extend(["--trust-config", temp_file.name]) # Fix-up the subcommand: the conformance suite uses `verify`, but # `sigstore` requires `verify identity` for identity based verifications. subcommand, *fixed_args = fixed_args if subcommand == "sign": if "--in-toto" in fixed_args: # Handle DSSE signing via library call fixed_args.remove("--in-toto") from sigstore.dsse import Statement from sigstore.models import ClientTrustConfig from sigstore.oidc import IdentityToken from sigstore.sign import SigningContext identity_token = None bundle_path = None try: i = fixed_args.index("--identity-token") identity_token = fixed_args[i + 1] i = fixed_args.index("--bundle") bundle_path = fixed_args[i + 1] except (ValueError, IndexError): raise ValueError("Missing required arguments for DSSE signing") # The statement is always the last argument in the protocol input_file = fixed_args[-1] # Direct library call with open(input_file, "rb") as f: statement_bytes = f.read() statement = Statement(statement_bytes) if trusted_root_path is not None: trust_config_obj = ClientTrustConfig.from_json(Path(temp_file.name).read_text()) else: trust_config_obj = ClientTrustConfig.production() if "--staging" in sys.argv: trust_config_obj = ClientTrustConfig.staging() context = SigningContext.from_trust_config(trust_config_obj) token = IdentityToken(identity_token) with context.signer(token) as signer: bundle = signer.sign_dsse(statement) with open(bundle_path, "w") as f: f.write(bundle.to_json()) # Exit successfully after handling DSSE sys.exit(0) command.append("sign") elif subcommand == "verify": command.extend(["verify", "identity"]) else: raise ValueError(f"unsupported subcommand: {subcommand}") # Replace incompatible flags. command.extend( ARG_REPLACEMENTS[arg] if arg in ARG_REPLACEMENTS else arg for arg in fixed_args ) os.execvp("sigstore", command) ================================================ FILE: test/unit/__init__.py ================================================ # 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. ================================================ FILE: test/unit/conftest.py ================================================ # 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. from __future__ import annotations import base64 import datetime import os import re from collections import defaultdict from collections.abc import Callable, Iterator from io import BytesIO from pathlib import Path from urllib.parse import urlparse import jwt import pytest from cryptography.x509 import Certificate, load_pem_x509_certificate from id import ( detect_credential, ) from tuf.api.exceptions import DownloadHTTPError from tuf.ngclient import FetcherInterface, updater from sigstore._internal import tuf from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._utils import sha256_digest from sigstore.models import Bundle, ClientTrustConfig from sigstore.oidc import IdentityToken from sigstore.sign import SigningContext from sigstore.verify.verifier import Verifier _TUF_ASSETS = (Path(__file__).parent.parent / "assets" / "staging-tuf").resolve() assert _TUF_ASSETS.is_dir() TEST_CLIENT_ID = "sigstore" @pytest.fixture def x509_testcase(asset): def _x509_testcase(name: str) -> Certificate: pem = asset(f"x509/{name}").read_bytes() return load_pem_x509_certificate(pem) return _x509_testcase @pytest.fixture def tuf_asset(): SHA256_TARGET_PATTERN = re.compile(r"[0-9a-f]{64}\.") class TUFAsset: def asset(self, name: str): return (_TUF_ASSETS / name).read_bytes() def target(self, name: str): path = self.target_path(name) return path.read_bytes() if path else None def target_path(self, name: str) -> Path: # Since TUF contains both sha256 and sha512 prefixed targets, filter # out the sha512 ones. matches = filter( lambda path: SHA256_TARGET_PATTERN.match(path.name) is not None, (_TUF_ASSETS / "targets").glob(f"*.{name}"), ) try: path = next(matches) except StopIteration as e: raise Exception(f"Unable to match {name} in targets/") from e if next(matches, None) is None: return path return None return TUFAsset() @pytest.fixture def signing_materials(asset) -> Callable[[str, RekorClient], tuple[Path, Bundle]]: # NOTE: Unlike `signing_bundle`, `signing_materials` requires a # Rekor client to retrieve its entry with. def _signing_materials(name: str, client: RekorClient) -> tuple[Path, Bundle]: file = asset(name) cert_path = asset(f"{name}.crt") sig_path = asset(f"{name}.sig") cert = load_pem_x509_certificate(cert_path.read_bytes()) sig = base64.b64decode(sig_path.read_text()) with file.open(mode="rb") as io: hashed = sha256_digest(io) entry = client.log.entries.retrieve.post( _hashedrekord_from_parts(cert, sig, hashed) ) bundle = Bundle.from_parts(cert, sig, entry) return (file, bundle) return _signing_materials @pytest.fixture def signing_bundle(asset) -> Callable[[str], tuple[Path, Bundle]]: def _signing_bundle(name: str) -> tuple[Path, Bundle]: file = asset(name) bundle_path = asset(f"{name}.sigstore") if not bundle_path.is_file(): bundle_path = asset(f"{name}.sigstore.json") bundle = Bundle.from_json(bundle_path.read_bytes()) return (file, bundle) return _signing_bundle @pytest.fixture def null_policy(): class NullPolicy: def verify(self, cert): return return NullPolicy() @pytest.fixture def mock_staging_tuf(monkeypatch, tuf_dirs): """Mock that prevents python-tuf from making requests: it returns staging assets from a local directory instead Return a tuple of dicts with the requested files and counts""" success = defaultdict(int) failure = defaultdict(int) class MockFetcher(FetcherInterface): def _fetch(self, url: str) -> Iterator[bytes]: filepath = _TUF_ASSETS / urlparse(url).path.lstrip("/") filename = filepath.name if filepath.is_file(): success[filename] += 1 return BytesIO(filepath.read_bytes()) failure[filename] += 1 raise DownloadHTTPError("File not found", 404) monkeypatch.setattr(updater, "Urllib3Fetcher", lambda app_user_agent: MockFetcher()) # Using the staging TUF assets is a nice way to test but staging tuf assets expire in # 3 days so faking now() becomes necessary. This correctly affects checks in # _internal/trust.py as well class mydatetime(datetime.datetime): @classmethod def now(cls, tz=None): return datetime.datetime(2025, 5, 6, 0, 0, 0, 0, datetime.timezone.utc) monkeypatch.setattr(datetime, "datetime", mydatetime) return success, failure @pytest.fixture def tuf_dirs(monkeypatch, tmp_path): # Patch _get_dirs as well, to avoid polluting the user's actual cache # with test assets. data_dir = tmp_path / "data" / "tuf" cache_dir = tmp_path / "cache" / "tuf" monkeypatch.setattr(tuf, "_get_dirs", lambda u: (data_dir, cache_dir)) return (data_dir, cache_dir) @pytest.fixture def sign_ctx_and_ident_for_env( pytestconfig, env: str, ) -> tuple[type[SigningContext], type[IdentityToken]]: """ Returns a SigningContext and IdentityToken for the given environment. The SigningContext is behind a callable so that it may be lazily evaluated. """ if env == "staging": def ctx_cls(): return SigningContext.from_trust_config(ClientTrustConfig.staging()) elif env == "production": def ctx_cls(): return SigningContext.from_trust_config(ClientTrustConfig.production()) else: raise ValueError(f"Unknown env {env}") token = os.getenv(f"SIGSTORE_IDENTITY_TOKEN_{env}") if not token: # If the variable is not defined, try getting an ambient token. token = detect_credential(TEST_CLIENT_ID) return ctx_cls, IdentityToken(token) @pytest.fixture def staging() -> tuple[type[SigningContext], type[Verifier], IdentityToken]: """ Returns a SigningContext, Verifier, and IdentityToken for the staging environment. The SigningContext and Verifier are both behind callables so that they may be lazily evaluated. """ def signer(): return SigningContext.from_trust_config(ClientTrustConfig.staging()) verifier = Verifier.staging # Detect env variable for local interactive tests. token = os.getenv("SIGSTORE_IDENTITY_TOKEN_staging") if not token: # If the variable is not defined, try getting an ambient token. token = detect_credential(TEST_CLIENT_ID) return signer, verifier, IdentityToken(token) @pytest.fixture def dummy_jwt(): def _dummy_jwt(claims: dict): return jwt.encode(claims, key="definitely not secure") return _dummy_jwt @pytest.fixture def tsa_url(): """Return the URL of the TSA""" return os.getenv("TEST_SIGSTORE_TIMESTAMP_AUTHORITY_URL") ================================================ FILE: test/unit/internal/__init__.py ================================================ # 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. ================================================ FILE: test/unit/internal/fulcio/__init__.py ================================================ # 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. ================================================ FILE: test/unit/internal/fulcio/test_client.py ================================================ # 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. ================================================ FILE: test/unit/internal/oidc/__init__.py ================================================ # 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. ================================================ FILE: test/unit/internal/oidc/test_issuer.py ================================================ # 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. from unittest.mock import MagicMock, patch import pytest from sigstore.oidc import IdentityError, Issuer, IssuerError @pytest.mark.online def test_fail_init_url(): with pytest.raises(IssuerError): Issuer("https://google.com") @pytest.mark.online def test_init_url(): Issuer("https://accounts.google.com") @pytest.mark.online def test_get_identity_token_bad_code(monkeypatch): # Send token request to oauth2.sigstage.dev but provide a bogus authorization code monkeypatch.setattr("builtins.input", lambda _: "hunter2") with pytest.raises(IdentityError, match=r"^Token request failed with .+$"): Issuer("https://oauth2.sigstage.dev/auth").identity_token(force_oob=True) def test_identity_token_csrf_protection(): """ Verify that identity_token() raises IdentityError when the returned state does not match the session state (CSRF protection). """ with ( patch("sigstore.oidc.webbrowser.open"), patch("sigstore._internal.oidc.oauth._OAuthFlow") as MockOAuthFlow, patch("sigstore.oidc.requests.Session") as MockSession, patch("sigstore.oidc.IdentityToken"), ): # Setup the mock server returned by _OAuthFlow context manager mock_server = MagicMock() MockOAuthFlow.return_value.__enter__.return_value = mock_server # Simulate a mismatching state original_state = "original-secure-state" malicious_state = "malicious-state" # The session has the original state (we mock the property access) # Since we added a property 'state', we need to make sure the mock returns it. # But here we are mocking the whole server object. # server.oauth_session.state mock_server.oauth_session.state = original_state mock_server.is_oob.return_value = False mock_server.base_uri = "http://localhost:12345" mock_server.redirect_uri = "http://localhost:12345/callback" # The auth response simulates what the redirect handler receives mock_server.auth_response = { "code": ["fake-code"], "state": [malicious_state], } # Mock responses for Issuer initialization and token exchange mock_session_instance = MockSession.return_value # Mock .well-known/openid-configuration response mock_config_response = MagicMock() mock_config_response.json.return_value = { "authorization_endpoint": "https://auth.example.com", "token_endpoint": "https://token.example.com", } mock_config_response.raise_for_status.return_value = None mock_session_instance.get.side_effect = [mock_config_response] # Initialize Issuer issuer = Issuer("https://issuer.example.com") # Call identity_token() and expect IdentityError due to state mismatch with pytest.raises(IdentityError, match="OAuth state mismatch"): issuer.identity_token() ================================================ FILE: test/unit/internal/rekor/__init__.py ================================================ # 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. ================================================ FILE: test/unit/internal/rekor/test_client_v2.py ================================================ # Copyright 2025 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. import hashlib import pytest from sigstore import dsse from sigstore.models import TransparencyLogEntry @pytest.mark.staging @pytest.mark.ambient_oidc def test_rekor_v2_create_entry_dsse(staging): # This is not a real unit test: it requires not only staging rekor but also TUF # fulcio and oidc -- maybe useful only until we have real integration tests in place sign_ctx_cls, _, identity = staging sign_ctx = sign_ctx_cls() stmt = ( dsse.StatementBuilder() .subjects( [ dsse.Subject( name="null", digest={"sha256": hashlib.sha256(b"").hexdigest()} ) ] ) .predicate_type("https://cosign.sigstore.dev/attestation/v1") .predicate( { "Data": "", "Timestamp": "2023-12-07T00:37:58Z", } ) ).build() with sign_ctx.signer(identity) as signer: bundle = signer.sign_dsse(stmt) assert isinstance(bundle.log_entry, TransparencyLogEntry) @pytest.mark.staging @pytest.mark.ambient_oidc def test_rekor_v2_create_entry_hashed_rekord(staging): # This is not a real unit test: it requires not only staging rekor but also TUF # fulcio and oidc -- maybe useful only until we have real integration tests in place sign_ctx_cls, _, identity = staging sign_ctx = sign_ctx_cls() with sign_ctx.signer(identity) as signer: bundle = signer.sign_artifact(b"") assert isinstance(bundle.log_entry, TransparencyLogEntry) ================================================ FILE: test/unit/internal/test_key_details.py ================================================ # Copyright 2025 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. from unittest.mock import Mock import pytest from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import dsa, ec, ed25519, padding, rsa from sigstore_models.common.v1 import PublicKeyDetails from sigstore._internal.key_details import _get_key_details # The algorithms tested below are from https://github.com/sigstore/fulcio/blob/4a86d8bf45972b58051ba44d91cd96664cf74711/cmd/app/serve.go#L125-L133 @pytest.mark.parametrize( "mock_certificate", [ # ec Mock( public_key=Mock( return_value=ec.generate_private_key(ec.SECP256R1()).public_key() ) ), Mock( public_key=Mock( return_value=ec.generate_private_key(ec.SECP384R1()).public_key() ) ), Mock( public_key=Mock( return_value=ec.generate_private_key(ec.SECP521R1()).public_key() ) ), # rsa pkcs1 Mock( public_key=Mock( return_value=rsa.generate_private_key( public_exponent=65537, key_size=2048 ).public_key() ), signature_algorithm_parameters=padding.PKCS1v15(), ), Mock( public_key=Mock( return_value=rsa.generate_private_key( public_exponent=65537, key_size=3072 ).public_key() ), signature_algorithm_parameters=padding.PKCS1v15(), ), Mock( public_key=Mock( return_value=rsa.generate_private_key( public_exponent=65537, key_size=4096 ).public_key() ), signature_algorithm_parameters=padding.PKCS1v15(), ), # ed25519 Mock( public_key=Mock( return_value=ed25519.Ed25519PrivateKey.generate().public_key(), signature_algorithm_parameters=None, ) ), ], ) def test_get_key_details(mock_certificate): """ Ensures that we return a PublicKeyDetails for supported key types and schemes. """ key_details = _get_key_details(mock_certificate) assert isinstance(key_details, PublicKeyDetails) def delayed_crypto_mock(mock_func, error_msg): # execute mock_func, mark test as skipped if cryptography does not support this algo. # This is done so missing support does not break the negative test collection try: data = mock_func() return pytest.param(data, error_msg) except UnsupportedAlgorithm as e: return pytest.param( None, error_msg, marks=pytest.mark.skip(reason=f"missing cryptography support: {e}"), ) class DummyCurve(ec.EllipticCurve): name = "dummycurve" @property def key_size(self): return 69420 @property def group_order(self): return 69420 @pytest.mark.parametrize( "mock_certificate, error_msg", [ # Unsupported EC curve delayed_crypto_mock( lambda: Mock( public_key=Mock( return_value=ec.generate_private_key(DummyCurve()).public_key() ) ), "Unsupported EC curve: dummycurve", ), # Unsupported RSA padding delayed_crypto_mock( lambda: Mock( public_key=Mock( return_value=rsa.generate_private_key( public_exponent=65537, key_size=2048 ).public_key() ), signature_algorithm_parameters=padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH, ), ), "Unsupported public key type, size, and padding", ), delayed_crypto_mock( lambda: Mock( public_key=Mock( return_value=rsa.generate_private_key( public_exponent=65537, key_size=3072 ).public_key() ), signature_algorithm_parameters=padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH, ), ), "Unsupported public key type, size, and padding", ), delayed_crypto_mock( lambda: Mock( public_key=Mock( return_value=rsa.generate_private_key( public_exponent=65537, key_size=4096 ).public_key() ), signature_algorithm_parameters=padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH, ), ), "Unsupported public key type, size, and padding", ), # Unsupported RSA key size delayed_crypto_mock( lambda: Mock( public_key=Mock( return_value=rsa.generate_private_key( public_exponent=65537, key_size=1024 ).public_key() ), signature_algorithm_parameters=padding.PKCS1v15(), ), "Unsupported RSA key size: 1024", ), # Unsupported key type delayed_crypto_mock( lambda: Mock( public_key=Mock( return_value=dsa.generate_private_key(key_size=1024).public_key() ) ), "Unsupported public key type", ), ], ) def test_get_key_details_unsupported(mock_certificate, error_msg): """ Ensures that we raise a ValueError for unsupported key types and schemes. """ with pytest.raises(ValueError, match=error_msg): _get_key_details(mock_certificate) ================================================ FILE: test/unit/internal/test_sct.py ================================================ # 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. import datetime import struct import pretend import pytest from cryptography.x509.certificate_transparency import LogEntryType from sigstore._internal import sct @pytest.mark.parametrize( "precert_bytes_len", [ 3, 255, 1024, 16777215, ], ) def test_pack_digitally_signed_precertificate(precert_bytes_len): precert_bytes = b"x" * precert_bytes_len mock_sct = pretend.stub( version=pretend.stub(value=0), timestamp=datetime.datetime.fromtimestamp( 1234 / 1000.0, tz=datetime.timezone.utc ), entry_type=LogEntryType.PRE_CERTIFICATE, extension_bytes=b"", ) cert = pretend.stub(tbs_precertificate_bytes=precert_bytes) issuer_key_hash = b"iamapublickeyshatwofivesixdigest" _, l1, l2, l3 = struct.unpack("!4c", struct.pack("!I", len(precert_bytes))) data = sct._pack_digitally_signed(mock_sct, cert, issuer_key_hash) assert data == ( b"\x00" # version b"\x00" # signature type b"\x00\x00\x00\x00\x00\x00\x04\xd2" # timestamp b"\x00\x01" # entry type b"iamapublickeyshatwofivesixdigest" # issuer key hash + l1 + l2 + l3 # tbs cert length + precert_bytes # tbs cert + b"\x00\x00" # extensions length + b"" # extensions ) ================================================ FILE: test/unit/internal/test_timestamping.py ================================================ # 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. import pytest import requests from sigstore._internal.timestamp import TimestampAuthorityClient, TimestampError from sigstore._utils import sha256_digest @pytest.mark.timestamp_authority class TestTimestampAuthorityClient: def test_sign_request(self, tsa_url: str): tsa = TimestampAuthorityClient(tsa_url) response = tsa.request_timestamp(b"hello") assert response assert ( response.tst_info.message_imprint.message == sha256_digest(b"hello").digest ) assert ( response.tst_info.message_imprint.hash_algorithm.dotted_string == "2.16.840.1.101.3.4.2.1" ) # SHA256 OID def test_sign_request_invalid_url(self): tsa = TimestampAuthorityClient("http://fake-url") with pytest.raises(TimestampError, match="error while sending"): tsa.request_timestamp(b"hello") def test_sign_request_invalid_request(self, tsa_url): tsa = TimestampAuthorityClient(tsa_url) with pytest.raises(TimestampError, match="invalid request"): tsa.request_timestamp(b"") # empty value here def test_invalid_response(self, tsa_url, monkeypatch): monkeypatch.setattr(requests.Response, "content", b"invalid-response") tsa = TimestampAuthorityClient(tsa_url) with pytest.raises(TimestampError, match="invalid response"): tsa.request_timestamp(b"hello") ================================================ FILE: test/unit/internal/test_trust.py ================================================ # 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. import os from datetime import datetime, timedelta, timezone import pytest from sigstore_models.common.v1 import TimeRange from sigstore_models.trustroot.v1 import ( Service, ServiceConfiguration, ServiceSelector, ) from sigstore._internal.fulcio.client import FulcioClient from sigstore._internal.rekor.client import RekorClient from sigstore._internal.rekor.client_v2 import RekorV2Client from sigstore._internal.timestamp import TimestampAuthorityClient from sigstore._internal.trust import ( CertificateAuthority, KeyringPurpose, ) from sigstore._utils import is_timerange_valid from sigstore.errors import Error, TUFError from sigstore.models import ( ClientTrustConfig, SigningConfig, TrustedRoot, ) # Test data for TestSigningcconfig _service_v1_op1 = Service(url="url1", major_api_version=1, operator="op1") _service2_v1_op1 = Service(url="url2", major_api_version=1, operator="op1") _service_v2_op1 = Service(url="url3", major_api_version=2, operator="op1") _service_v1_op2 = Service(url="url4", major_api_version=1, operator="op2") _service_v1_op3 = Service(url="url5", major_api_version=1, operator="op3") _service_v1_op4 = Service( url="url6", major_api_version=1, operator="op4", valid_for=TimeRange(start=datetime(3000, 1, 1, tzinfo=timezone.utc)), ) class TestCertificateAuthority: def test_good(self, asset): path = asset("trusted_root/certificate_authority.json") authority = CertificateAuthority.from_json(path) assert len(authority.certificates(allow_expired=True)) == 3 assert authority.validity_period_end is not None assert authority.validity_period_start < authority.validity_period_end def test_missing_root(self, asset): path = asset("trusted_root/certificate_authority.empty.json") with pytest.raises(Error, match="missing a certificate"): CertificateAuthority.from_json(path) class TestSigningConfig: def test_good(self, asset): path = asset("signing_config/signingconfig.v2.json") signing_config = SigningConfig.from_file(path) assert ( signing_config._inner.media_type == SigningConfig.SigningConfigType.SIGNING_CONFIG_0_2.value ) fulcio = signing_config.get_fulcio() assert isinstance(fulcio, FulcioClient) assert fulcio.url == "https://fulcio.example.com" assert signing_config.get_oidc_url() == "https://oauth2.example.com/auth" # signing config contains v1 and v2, we pick v2 tlogs = signing_config.get_tlogs() assert len(tlogs) == 1 assert isinstance(tlogs[0], RekorV2Client) assert tlogs[0].url == "https://rekor-v2.example.com/api/v2" tsas = signing_config.get_tsas() assert len(tsas) == 1 assert isinstance(tsas[0], TimestampAuthorityClient) assert tsas[0].url == "https://timestamp.example.com/api/v1/timestamp" def test_good_only_v1_rekor(self, asset): """Test case where a rekor 2 instance is not available""" path = asset("signing_config/signingconfig-only-v1-rekor.v2.json") signing_config = SigningConfig.from_file(path) tlogs = signing_config.get_tlogs() assert len(tlogs) == 1 assert isinstance(tlogs[0], RekorClient) assert tlogs[0].url == "https://rekor.example.com/api/v1" @pytest.mark.parametrize( "services, versions, config, expected_result", [ pytest.param( [_service_v1_op1], [1], ServiceConfiguration(selector=ServiceSelector.ALL), [_service_v1_op1], id="base case", ), pytest.param( [_service_v1_op1, _service2_v1_op1], [1], ServiceConfiguration(selector=ServiceSelector.ALL), [_service2_v1_op1], id="multiple services, same operator: expect 1 service in result", ), pytest.param( [_service_v1_op1, _service_v1_op2], [1], ServiceConfiguration(selector=ServiceSelector.ALL), [_service_v1_op1, _service_v1_op2], id="2 services, different operator: expect 2 services in result", ), pytest.param( [_service_v1_op1, _service_v1_op2, _service_v1_op4], [1], ServiceConfiguration(selector=ServiceSelector.ALL), [_service_v1_op1, _service_v1_op2], id="3 services, one is not yet valid: expect 2 services in result", ), pytest.param( [_service_v1_op1, _service_v1_op2], [1], ServiceConfiguration(selector=ServiceSelector.ANY), [_service_v1_op1], id="ANY selector: expect 1 service only in result", ), pytest.param( [_service_v1_op1, _service_v1_op2, _service_v1_op3], [1], ServiceConfiguration(selector=ServiceSelector.EXACT, count=2), [_service_v1_op1, _service_v1_op2], id="EXACT selector: expect configured number of services in result", ), pytest.param( [_service_v1_op1, _service_v2_op1], [1, 2], ServiceConfiguration(selector=ServiceSelector.ALL), [_service_v2_op1], id="services with different version: expect highest version", ), pytest.param( [_service_v1_op1, _service_v2_op1], [1], ServiceConfiguration(selector=ServiceSelector.ALL), [_service_v1_op1], id="services with different version: expect the supported version", ), pytest.param( [_service_v1_op1, _service_v1_op2], [2], ServiceConfiguration(selector=ServiceSelector.ALL), [], id="No supported versions: expect no results", ), pytest.param( [_service_v1_op1, _service_v2_op1, _service_v1_op2], [1], None, [_service_v1_op1, _service_v1_op2], id="services without ServiceConfiguration: expect all supported", ), ], ) def test_get_valid_services(self, services, versions, config, expected_result): result = SigningConfig._get_valid_services(services, versions, config) assert result == expected_result @pytest.mark.parametrize( "services, versions, config", [ ( # EXACT selector without enough services [_service_v1_op1], [1], ServiceConfiguration(selector=ServiceSelector.EXACT, count=2), ), ], ) def test_get_valid_services_fail(self, services, versions, config): with pytest.raises(ValueError): SigningConfig._get_valid_services(services, versions, config) class TestTrustedRoot: @pytest.mark.parametrize( "file", [ "trusted_root/trustedroot.v1.json", "trusted_root/trustedroot.v1.local_tlog_ed25519_rekor-tiles.json", ], ) def test_good(self, asset, file): """ Ensures that the trusted_roots are well-formed and that the expected embedded keys are supported. """ path = asset(file) root = TrustedRoot.from_file(path) assert ( root._inner.media_type == TrustedRoot.TrustedRootType.TRUSTED_ROOT_0_1.value ) assert len(root._inner.tlogs) == 1 assert len(root._inner.certificate_authorities) == 2 assert len(root._inner.ctlogs) == 2 assert len(root._inner.timestamp_authorities) == 1 # only one of the two rekor keys is actually supported assert len(root.rekor_keyring(KeyringPurpose.VERIFY)._keyring) == 1 assert len(root.ct_keyring(KeyringPurpose.VERIFY)._keyring) == 2 assert root.get_fulcio_certs() is not None assert root.get_timestamp_authorities() is not None def test_bad_media_type(self, asset): path = asset("trusted_root/trustedroot.badtype.json") with pytest.raises( ValueError, match=r"Input should be 'application/vnd\.dev\.sigstore\.trustedroot\+json;version=0\.1' or 'application/vnd\.dev\.sigstore\.trustedroot\.v0\.2\+json'", ): TrustedRoot.from_file(path) # TODO(ww): Move these into appropriate class-scoped tests. def test_trust_root_tuf_offline(mock_staging_tuf, tuf_dirs): # start with empty target cache, empty local metadata dir data_dir, cache_dir = tuf_dirs # keep track of requests the TrustUpdater invoked by TrustedRoot makes reqs, fail_reqs = mock_staging_tuf trust_config = ClientTrustConfig.staging(offline=True) # local TUF metadata is not initialized, nothing is downloaded assert not os.path.exists(data_dir) assert reqs == {} assert fail_reqs == {} trust_config.trusted_root.ct_keyring(purpose=KeyringPurpose.VERIFY) trust_config.trusted_root.rekor_keyring(purpose=KeyringPurpose.VERIFY) # Still no requests assert reqs == {} assert fail_reqs == {} def test_is_timerange_valid(): def range_from(offset_lower=0, offset_upper=0): base = datetime.now(timezone.utc) return TimeRange( start=base + timedelta(minutes=offset_lower), end=base + timedelta(minutes=offset_upper), ) # Test None should always be valid assert is_timerange_valid(None, allow_expired=False) assert is_timerange_valid(None, allow_expired=True) # Test lower bound conditions assert is_timerange_valid( range_from(-1, 1), allow_expired=False ) # Valid: 1 ago, 1 from now assert not is_timerange_valid( range_from(1, 1), allow_expired=False ) # Invalid: 1 from now, 1 from now # Test upper bound conditions assert not is_timerange_valid( range_from(-1, -1), allow_expired=False ) # Invalid: 1 ago, 1 ago assert is_timerange_valid( range_from(-1, -1), allow_expired=True ) # Valid: 1 ago, 1 ago def test_trust_root_tuf_instance_error(): # embedded root.json is not found and no local metadata is found with pytest.raises(TUFError): ClientTrustConfig.from_tuf("foo.bar") def test_trust_root_tuf_ctfe_keys_error(monkeypatch): trust_root = ClientTrustConfig.staging(offline=True).trusted_root monkeypatch.setattr(trust_root._inner, "ctlogs", []) with pytest.raises(Exception, match="CTFE keys not found in trusted root"): trust_root.ct_keyring(purpose=KeyringPurpose.VERIFY) def test_trust_root_fulcio_certs_error(tuf_asset, monkeypatch): trust_root = ClientTrustConfig.staging(offline=True).trusted_root monkeypatch.setattr(trust_root._inner, "certificate_authorities", []) with pytest.raises( Exception, match="Fulcio certificates not found in trusted root" ): trust_root.get_fulcio_certs() class TestClientTrustConfig: def test_good(self, asset): path = asset("trust_config/config.v1.json") config = ClientTrustConfig.from_json(path.read_text()) assert isinstance(config.signing_config, SigningConfig) assert isinstance(config.trusted_root, TrustedRoot) def test_bad_media_type(self, asset): path = asset("trust_config/config.badtype.json") with pytest.raises( ValueError, match=r"Input should be 'application/vnd\.dev\.sigstore\.clienttrustconfig.v0.1\+json'", ): ClientTrustConfig.from_json(path.read_text()) ================================================ FILE: test/unit/test_dsse.py ================================================ # 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. import base64 import json import pytest from sigstore import dsse from sigstore.dsse import InvalidEnvelope class TestEnvelope: def test_roundtrip(self): raw = json.dumps( { "payload": base64.b64encode(b"foo").decode(), "payloadType": dsse.Envelope._TYPE, "signatures": [ {"sig": base64.b64encode(b"lol").decode()}, ], } ) evp = dsse.Envelope._from_json(raw) assert evp._inner.payload == b"foo" assert evp._inner.payload_type == dsse.Envelope._TYPE assert evp.signature == b"lol" serialized = evp.to_json() # envelope matches assert dsse.Envelope._from_json(serialized) == evp # parsed JSON marches assert json.loads(raw) == evp._inner.to_dict() def test_missing_signature(self): raw = json.dumps( { "payload": base64.b64encode(b"foo").decode(), "payloadType": dsse.Envelope._TYPE, "signatures": [], } ) with pytest.raises(InvalidEnvelope, match="one signature"): dsse.Envelope._from_json(raw) def test_empty_signature(self): raw = json.dumps( { "payload": base64.b64encode(b"foo").decode(), "payloadType": dsse.Envelope._TYPE, "signatures": [ {"sig": ""}, ], } ) with pytest.raises(InvalidEnvelope, match="non-empty"): dsse.Envelope._from_json(raw) def test_multiple_signatures(self): raw = json.dumps( { "payload": base64.b64encode(b"foo").decode(), "payloadType": dsse.Envelope._TYPE, "signatures": [ {"sig": base64.b64encode(b"lol").decode()}, {"sig": base64.b64encode(b"lmao").decode()}, ], } ) with pytest.raises(InvalidEnvelope, match="one signature"): dsse.Envelope._from_json(raw) ================================================ FILE: test/unit/test_hashes.py ================================================ # Copyright 2024 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. import hashlib import pytest from sigstore_models.common.v1 import HashAlgorithm from sigstore.hashes import Hashed class TestHashes: @pytest.mark.parametrize( ("algorithm", "digest"), [ (HashAlgorithm.SHA2_256, hashlib.sha256(b"").hexdigest()), (HashAlgorithm.SHA2_384, hashlib.sha384(b"").hexdigest()), (HashAlgorithm.SHA2_512, hashlib.sha512(b"").hexdigest()), (HashAlgorithm.SHA3_256, hashlib.sha3_256(b"").hexdigest()), (HashAlgorithm.SHA3_384, hashlib.sha3_384(b"").hexdigest()), ], ) def test_hashed_repr(self, algorithm, digest): hashed = Hashed(algorithm=algorithm, digest=bytes.fromhex(digest)) assert str(hashed) == f"{algorithm.name}:{digest}" ================================================ FILE: test/unit/test_models.py ================================================ # 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. import json from base64 import b64encode import pytest from sigstore_models.rekor.v1 import KindVersion from sigstore_models.rekor.v1 import TransparencyLogEntry as _TransparencyLogEntry from sigstore.errors import VerificationError from sigstore.models import ( Bundle, InvalidBundle, TimestampVerificationData, TransparencyLogEntry, VerificationMaterial, ) class TestTransparencyLogEntry: @pytest.mark.parametrize("integrated_time", [0, 1746819403]) def test_missing_inclusion_proof(self, integrated_time: int): with pytest.raises(ValueError, match=r"inclusion_proof"): TransparencyLogEntry( _TransparencyLogEntry( kind_version=KindVersion(kind="hashedrekord", version="fake"), canonicalized_body=b64encode(b"fake"), integrated_time=integrated_time, log_id="1234", log_index=1, inclusion_proof=None, inclusion_promise=None, ) ) # def test_missing_inclusion_promise_and_integrated_time_round_trip( # self, signing_bundle # ): # """ # Ensures that LogEntry._to_rekor() succeeds even without an inclusion_promise and integrated_time. # """ # bundle: Bundle # _, bundle = signing_bundle("bundle.txt") # _dict = bundle.log_entry._to_rekor().to_dict() # print(_dict) # del _dict["inclusionPromise"] # del _dict["integratedTime"] # entry = LogEntry._from_dict_rekor(_dict) # assert entry.inclusion_promise is None # assert entry._to_rekor() is not None # assert LogEntry._from_dict_rekor(entry._to_rekor().to_dict()) == entry def test_logentry_roundtrip(self, signing_bundle): _, bundle = signing_bundle("bundle.txt") assert ( TransparencyLogEntry( _TransparencyLogEntry.from_dict(bundle.log_entry._inner.to_dict()) ) == bundle.log_entry ) class TestTimestampVerificationData: """ Tests for the `TimestampVerificationData` wrapper model. """ def test_valid_timestamp(self, asset): timestamp = { "rfc3161Timestamps": [ { "signedTimestamp": "MIIEgTADAgEAMIIEeAYJKoZIhvcNAQcCoIIEaTCCBGUCAQMxDTALBglghkgBZQMEAgEwgc8GCyqGSIb3DQEJEAEEoIG/BIG8MIG5AgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgyGobd7rprYIL0JTus5EpEb7jrrecS+cMbb42ftjtm+UCFBV/kwOOwt0tdtYXK1FGhXf7W4oFGA8yMDI0MTAyMjA3MzEwNVowAwIBAQIUTo190a2ixXglxLh7KJcwj6B4kf+gNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHRMIIBzTCCAXKgAwIBAgIUIYzlmDAtGrQ5jmcZpeAN0Wyj8Q8wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMjIwNzIyNTNaFw0zMzEwMjIwNzI1NTNaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQBhKWvDUj1+VFrWudnWIRzAug99WAydJuyF9pxneWppyXbjio3RSoNBvhg+91eeue7GpRQx5ZoxdeiHJD5p7Z0o2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFD7JreyIuE9lHC9k+cFePRXIPdNaMB8GA1UdIwQYMBaAFJMEP2b7r8olhCtvCokuFyTMC0nOMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0kAMEYCIQC69iKNrM4N2/OHksX7zEJM7ImGR+Puq7ALM8l3+riChgIhAKbEWTmifAE6VaQwnL0NNTJskSgk6r8BzvbJtJEZpk6fMYIBqDCCAaQCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQhjOWYMC0atDmOZxml4A3RbKPxDzALBglghkgBZQMEAgGggfMwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMjIwNzMxMDVaMC8GCSqGSIb3DQEJBDEiBCBr9fx6gIRsipdGxMDIw1tpvHUv3y10SHUzEM+HHP15+DCBhQYLKoZIhvcNAQkQAi8xdjB0MHIwcAQg2PR1japGgjWt7Cd0jQJrSYlYTblz/UeoJw0LkbqIsSIwTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIUIYzlmDAtGrQ5jmcZpeAN0Wyj8Q8wCgYIKoZIzj0EAwIERjBEAiBDfeCcnA1qIlHfMK/u3FZ1HtS9840NnXXaRdMD4R7MywIgZfoBiAMV3SFqO71+eo2kD9oBkW49Pb9eoQs00nOlvn8=" } ] } timestamp_verification = TimestampVerificationData.from_json( json.dumps(timestamp) ) assert timestamp_verification.rfc3161_timestamps def test_no_timestamp(self, asset): timestamp = {"rfc3161Timestamps": []} timestamp_verification = TimestampVerificationData.from_json( json.dumps(timestamp) ) assert not timestamp_verification.rfc3161_timestamps def test_invalid_timestamp(self, asset): timestamp = {"rfc3161Timestamps": [{"signedTimestamp": "invalid-entry"}]} with pytest.raises(VerificationError, match="Invalid Timestamp"): TimestampVerificationData.from_json(json.dumps(timestamp)) class TestVerificationMaterial: """ Tests for the `VerificationMaterial` wrapper model. """ def test_valid_verification_material(self, asset): bundle = Bundle.from_json(asset("bundle.txt.sigstore").read_bytes()) verification_material = VerificationMaterial( bundle._inner.verification_material ) assert verification_material class TestBundle: """ Tests for the `Bundle` wrapper model. """ def test_invalid_bundle_version(self, signing_bundle): with pytest.raises(InvalidBundle, match="failed to load bundle"): signing_bundle("bundle_invalid_version.txt") def test_invalid_empty_cert_chain(self, signing_bundle): with pytest.raises( InvalidBundle, match="expected non-empty certificate chain in bundle" ): signing_bundle("bundle_no_cert_v1.txt") def test_invalid_no_log_entry(self, signing_bundle): with pytest.raises( InvalidBundle, match="expected exactly one log entry in bundle" ): signing_bundle("bundle_no_log_entry.txt") def test_verification_materials_offline_no_checkpoint(self, signing_bundle): with pytest.raises( InvalidBundle, match="entry must contain inclusion proof, with checkpoint" ): signing_bundle("bundle_no_checkpoint.txt") def test_bundle_roundtrip(self, signing_bundle): _, bundle = signing_bundle("bundle.txt") # Bundles are not directly comparable, but a round-trip preserves their # underlying object structure. assert json.loads(Bundle.from_json(bundle.to_json()).to_json()) == json.loads( bundle.to_json() ) def test_bundle_missing_signed_time(self, signing_bundle): with pytest.raises( InvalidBundle, match=r"bundle must contain an inclusion promise or signed timestamp\(s\)", ): signing_bundle("bundle_v3_no_signed_time.txt") class TestKnownBundleTypes: def test_str(self): for type_ in Bundle.BundleType: assert str(type_) == type_.value assert type_ in Bundle.BundleType ================================================ FILE: test/unit/test_oidc.py ================================================ # 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. import datetime import pytest from sigstore import oidc class TestIdentityToken: def test_invalid_jwt(self): with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken("invalid jwt") def test_missing_iss(self, dummy_jwt): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": now, "nbf": now, "exp": now + 600, } ) with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken(jwt) def test_missing_aud(self, dummy_jwt): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "sub": "fakesubject", "iat": now, "nbf": now, "exp": now + 600, "iss": "fake-issuer", } ) with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken(jwt) @pytest.mark.parametrize("aud", (None, "not-sigstore")) def test_invalid_aud(self, dummy_jwt, aud): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": aud, "sub": "fakesubject", "iat": now, "nbf": now, "exp": now + 600, "iss": "fake-issuer", } ) with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken(jwt) def test_missing_iat(self, dummy_jwt): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "nbf": now, "exp": now + 600, "iss": "fake-issuer", } ) with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken(jwt) @pytest.mark.parametrize("iat", (None, "not-an-int")) def test_invalid_iat(self, dummy_jwt, iat): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": iat, "nbf": now, "exp": now + 600, "iss": "fake-issuer", } ) with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken(jwt) def test_missing_nbf_ok(self, dummy_jwt): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "iat": now, "exp": now + 600, "iss": "fake-issuer", "sub": "sigstore", } ) assert oidc.IdentityToken(jwt) is not None def test_invalid_nbf(self, dummy_jwt): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": now, "nbf": now + 600, "exp": now + 601, "iss": "fake-issuer", } ) with pytest.raises( oidc.IdentityError, match="Identity token is not within its validity period", ): oidc.IdentityToken(jwt) def test_missing_exp(self, dummy_jwt): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": now, "nbf": now, "iss": "fake-issuer", } ) with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken(jwt) def test_invalid_exp(self, dummy_jwt): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": now - 600, "nbf": now - 300, # NOTE: 6 seconds due to +/- 5 second flutter. "exp": now - 6, "iss": "fake-issuer", } ) with pytest.raises( oidc.IdentityError, match="Identity token is malformed or missing claims" ): oidc.IdentityToken(jwt) @pytest.mark.parametrize( "iss", [k for k, v in oidc._KNOWN_OIDC_ISSUERS.items() if v != "sub"] ) def test_missing_identity_claim(self, dummy_jwt, iss): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": now, "nbf": now, "exp": now + 600, "iss": iss, } ) with pytest.raises( oidc.IdentityError, match=r"Identity token is missing the required '.+' claim", ): oidc.IdentityToken(jwt) @pytest.mark.parametrize("fed", ("notadict", {"connector_id": 123})) def test_invalid_federated_claims(self, dummy_jwt, fed): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": now, "nbf": now, "exp": now + 600, "iss": "https://accounts.google.com", "email": "example@example.com", "federated_claims": fed, } ) with pytest.raises( oidc.IdentityError, match="unexpected claim type: federated_claims.*", ): oidc.IdentityToken(jwt) @pytest.mark.parametrize( ("iss", "identity_claim", "identity_value", "fed_iss"), [ ("https://accounts.google.com", "email", "example@example.com", None), ( "https://oauth2.sigstore.dev/auth", "email", "example@example.com", "https://accounts.google.com", ), ("https://oauth2.sigstore.dev/auth", "email", "example@example.com", None), ( "https://token.actions.githubusercontent.com", "sub", "some-subject", None, ), ("hxxps://unknown.issuer.example.com/auth", "sub", "some-subject", None), ], ) def test_ok(self, dummy_jwt, iss, identity_claim, identity_value, fed_iss): now = int(datetime.datetime.now().timestamp()) jwt = dummy_jwt( { "aud": "sigstore", "sub": "fakesubject", "iat": now, "nbf": now, "exp": now + 600, "iss": iss, identity_claim: identity_value, "federated_claims": {"connector_id": fed_iss}, } ) identity = oidc.IdentityToken(jwt) assert identity.in_validity_period() assert identity.identity == identity_value assert identity.issuer == iss assert identity.federated_issuer == iss if not fed_iss else fed_iss ================================================ FILE: test/unit/test_session_reuse.py ================================================ # Copyright 2026 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. import unittest.mock from sigstore._internal.rekor import EntryRequestBody from sigstore._internal.rekor.client import RekorClient from sigstore._internal.rekor.client_v2 import RekorV2Client from sigstore._internal.timestamp import TimestampAuthorityClient def test_rekor_v1_session_reuse_public_api(): """Verify that RekorClient v1 reuses its session per thread using public API.""" client = RekorClient("http://fake") with unittest.mock.patch("requests.Session") as mock_session_cls: mock_session_inst = unittest.mock.MagicMock() mock_session_cls.return_value = mock_session_inst # Access log endpoint multiple times client.log client.log # Expect 1 session assert mock_session_cls.call_count == 1 def test_rekor_v2_session_reuse_public_api(): """Verify that RekorV2Client reuses its session per thread using public API.""" client = RekorV2Client("http://fake") with unittest.mock.patch("requests.Session") as mock_session_cls: mock_session_inst = unittest.mock.MagicMock() mock_session_cls.return_value = mock_session_inst # Call create_entry multiple times (hide exception: the client does not need to work) try: client.create_entry(EntryRequestBody({})) except Exception: pass try: client.create_entry(EntryRequestBody({})) except Exception: pass # Expect 1 session assert mock_session_cls.call_count == 1 def test_timestamp_client_session_reuse_public_api(): """Verify that TimestampAuthorityClient reuses its session per thread using public API.""" client = TimestampAuthorityClient("http://fake") with unittest.mock.patch("requests.Session") as mock_session_cls: mock_session_inst = unittest.mock.MagicMock() mock_session_cls.return_value = mock_session_inst # Call request_timestamp multiple times (hide exception: the client does not need to work) try: client.request_timestamp(b"sig") except Exception: pass try: client.request_timestamp(b"sig") except Exception: pass # Expect 1 session assert mock_session_cls.call_count == 1 ================================================ FILE: test/unit/test_sign.py ================================================ # 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. import hashlib import secrets import pretend import pytest from sigstore_models.common.v1 import HashAlgorithm import sigstore.oidc from sigstore.dsse import StatementBuilder, Subject from sigstore.errors import VerificationError from sigstore.hashes import Hashed from sigstore.sign import SigningContext from sigstore.verify.policy import UnsafeNoOp # only check the log contents for production: staging is already on # rekor v2 and we don't currently support log lookups on rekor v2. # This test can likely be removed once prod also uses rekor v2 @pytest.mark.parametrize("env", ["production"]) @pytest.mark.ambient_oidc def test_sign_rekor_entry_consistent(request, sign_ctx_and_ident_for_env): ctx_cls, identity = sign_ctx_and_ident_for_env # NOTE: The actual signer instance is produced lazily, so that parameter # expansion doesn't fail in offline tests. ctx: SigningContext = ctx_cls() assert identity is not None payload = secrets.token_bytes(32) with ctx.signer(identity) as signer: expected_entry = signer.sign_artifact(payload).log_entry actual_entry = ctx._rekor.log.entries.get(log_index=expected_entry._inner.log_index) assert ( expected_entry._inner.canonicalized_body == actual_entry._inner.canonicalized_body ) assert expected_entry._inner.integrated_time == actual_entry._inner.integrated_time assert expected_entry._inner.log_id == actual_entry._inner.log_id assert expected_entry._inner.log_index == actual_entry._inner.log_index @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_with_staging(staging, null_policy): ctx_cls, verifier_cls, identity = staging ctx: SigningContext = ctx_cls() verifier = verifier_cls() assert identity is not None payload = secrets.token_bytes(32) with ctx.signer(identity) as signer: bundle = signer.sign_artifact(payload) verifier.verify_artifact(payload, bundle, null_policy) @pytest.mark.parametrize("env", ["staging", "production"]) @pytest.mark.ambient_oidc def test_sct_verify_keyring_lookup_error(sign_ctx_and_ident_for_env, monkeypatch): ctx, identity = sign_ctx_and_ident_for_env # a signer whose keyring always fails to lookup a given key. ctx: SigningContext = ctx() mock = pretend.stub( ct_keyring=lambda *a: pretend.stub(verify=pretend.raiser(VerificationError)) ) ctx._trusted_root = mock assert identity is not None payload = secrets.token_bytes(32) with pytest.raises(VerificationError, match=r"SCT verify failed:"): with ctx.signer(identity) as signer: signer.sign_artifact(payload) @pytest.mark.parametrize("env", ["staging", "production"]) @pytest.mark.ambient_oidc def test_sct_verify_keyring_error(sign_ctx_and_ident_for_env, monkeypatch): ctx, identity = sign_ctx_and_ident_for_env # a signer whose keyring throws an internal error. ctx: SigningContext = ctx() mock = pretend.stub( ct_keyring=lambda *a: pretend.stub(verify=pretend.raiser(VerificationError)) ) ctx._trusted_root = mock assert identity is not None payload = secrets.token_bytes(32) with pytest.raises(VerificationError): with ctx.signer(identity) as signer: signer.sign_artifact(payload) @pytest.mark.parametrize("env", ["staging", "production"]) @pytest.mark.ambient_oidc def test_identity_proof_fallback_claim(sign_ctx_and_ident_for_env, monkeypatch): ctx_cls, identity = sign_ctx_and_ident_for_env ctx: SigningContext = ctx_cls() assert identity is not None # clear out known issuers, forcing the `Identity`'s `sub` claim to be used # as fall back monkeypatch.setattr(sigstore.oidc, "_KNOWN_OIDC_ISSUERS", {}) payload = secrets.token_bytes(32) with ctx.signer(identity) as signer: signer.sign_artifact(payload) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_prehashed(staging): sign_ctx_cls, verifier_cls, identity = staging sign_ctx = sign_ctx_cls() verifier = verifier_cls() input_ = secrets.token_bytes(32) hashed = Hashed( digest=hashlib.sha256(input_).digest(), algorithm=HashAlgorithm.SHA2_256 ) with sign_ctx.signer(identity) as signer: bundle = signer.sign_artifact(hashed) assert bundle._inner.message_signature.message_digest.algorithm == hashed.algorithm assert bundle._inner.message_signature.message_digest.digest == hashed.digest # verifying against the original input works verifier.verify_artifact(input_, bundle=bundle, policy=UnsafeNoOp()) # verifying against the prehash also works verifier.verify_artifact(hashed, bundle=bundle, policy=UnsafeNoOp()) @pytest.mark.staging @pytest.mark.ambient_oidc def test_sign_dsse(staging): sign_ctx, _, identity = staging ctx = sign_ctx() stmt = ( StatementBuilder() .subjects( [Subject(name="null", digest={"sha256": hashlib.sha256(b"").hexdigest()})] ) .predicate_type("https://cosign.sigstore.dev/attestation/v1") .predicate( { "Data": "", "Timestamp": "2023-12-07T00:37:58Z", } ) ).build() with ctx.signer(identity) as signer: bundle = signer.sign_dsse(stmt) # Ensures that all of our inner types serialize as expected. bundle.to_json() ================================================ FILE: test/unit/test_store.py ================================================ # 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. import json import pytest from sigstore._utils import read_embedded @pytest.mark.parametrize( "env", [ "https://tuf-repo-cdn.sigstore.dev", "https://tuf-repo-cdn.sigstage.dev", ], ) def test_store_reads_root_json(env): root_json = read_embedded("root.json", env) assert json.loads(root_json) @pytest.mark.parametrize( "env", [ "https://tuf-repo-cdn.sigstore.dev", "https://tuf-repo-cdn.sigstage.dev", ], ) def test_store_reads_targets_json(env): trusted_root_json = read_embedded("trusted_root.json", env) assert json.loads(trusted_root_json) ================================================ FILE: test/unit/test_utils.py ================================================ # 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. import hashlib import io import pretend import pytest from cryptography import x509 from cryptography.hazmat.primitives import serialization from sigstore import _utils as utils from sigstore.errors import VerificationError def test_key_id(): # Taken from certificate-transparency-go: # https://github.com/google/certificate-transparency-go/blob/88227ce0/trillian/ctfe/testonly/certificates.go#L213-L231 precert_pem = b"""-----BEGIN CERTIFICATE----- MIIC3zCCAkigAwIBAgIBBzANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJHQjEk MCIGA1UEChMbQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5IENBMQ4wDAYDVQQIEwVX YWxlczEQMA4GA1UEBxMHRXJ3IFdlbjAeFw0xMjA2MDEwMDAwMDBaFw0yMjA2MDEw MDAwMDBaMFIxCzAJBgNVBAYTAkdCMSEwHwYDVQQKExhDZXJ0aWZpY2F0ZSBUcmFu c3BhcmVuY3kxDjAMBgNVBAgTBVdhbGVzMRAwDgYDVQQHEwdFcncgV2VuMIGfMA0G CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+75jnwmh3rjhfdTJaDB0ym+3xj6r015a/ BH634c4VyVui+A7kWL19uG+KSyUhkaeb1wDDjpwDibRc1NyaEgqyHgy0HNDnKAWk EM2cW9tdSSdyba8XEPYBhzd+olsaHjnu0LiBGdwVTcaPfajjDK8VijPmyVCfSgWw FAn/Xdh+tQIDAQABo4HBMIG+MB0GA1UdDgQWBBQgMVQa8lwF/9hli2hDeU9ekDb3 tDB9BgNVHSMEdjB0gBRfnYgNyHPmVNT4DdjmsMEktEfDVaFZpFcwVTELMAkGA1UE BhMCR0IxJDAiBgNVBAoTG0NlcnRpZmljYXRlIFRyYW5zcGFyZW5jeSBDQTEOMAwG A1UECBMFV2FsZXMxEDAOBgNVBAcTB0VydyBXZW6CAQAwCQYDVR0TBAIwADATBgor BgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0BAQUFAAOBgQACocOeAVr1Tf8CPDNg h1//NDdVLx8JAb3CVDFfM3K3I/sV+87MTfRxoM5NjFRlXYSHl/soHj36u0YtLGhL BW/qe2O0cP8WbjLURgY1s9K8bagkmyYw5x/DTwjyPdTuIo+PdPY9eGMR3QpYEUBf kGzKLC0+6/yBmWTr2M98CIY/vg== -----END CERTIFICATE-----""" precert = x509.load_pem_x509_certificate(precert_pem) public_key = precert.public_key().public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) key_id = utils.key_id(precert.public_key()) assert key_id == hashlib.sha256(public_key).digest() assert ( hashlib.sha256(public_key).hexdigest() == "086c0ea25b60e3c44a994d0d5f40b81a0d44f21d63df19315e6ddfbe47373817" ) @pytest.mark.parametrize( "size", [0, 1, 2, 4, 8, 32, 128, 1024, 128 * 1024, 1024 * 1024, 128 * 1024 * 1024] ) def test_sha256_streaming(size): buf = b"x" * size expected_digest = hashlib.sha256(buf).digest() actual_digest = utils._sha256_streaming(io.BytesIO(buf)) assert expected_digest == actual_digest def test_load_pem_public_key_format(): keybytes = b"-----BEGIN PUBLIC KEY-----\nbleh\n-----END PUBLIC KEY-----" with pytest.raises( VerificationError, match="could not load PEM-formatted public key" ): utils.load_pem_public_key([keybytes]) def test_load_pem_public_key_serialization(monkeypatch): from cryptography.hazmat.primitives import serialization monkeypatch.setattr(serialization, "load_pem_public_key", lambda a: a) keybytes = ( b"-----BEGIN PUBLIC KEY-----\n" b"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu\n" b"dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\n" b"-----END PUBLIC KEY-----" ) with pytest.raises(VerificationError, match="invalid key format: not one of"): utils.load_pem_public_key([keybytes]) @pytest.mark.parametrize( ("testcase", "valid"), [ ("bogus-root.pem", True), ("bogus-intermediate.pem", True), ("bogus-leaf.pem", False), ], ) def test_cert_is_ca(x509_testcase, testcase, valid): cert = x509_testcase(testcase) assert utils.cert_is_ca(cert) is valid @pytest.mark.parametrize( "testcase", [ "bogus-root-noncritical-bc.pem", "bogus-root-invalid-ku.pem", "bogus-root-missing-ku.pem", ], ) def test_cert_is_ca_invalid_states(x509_testcase, testcase): cert = x509_testcase(testcase) with pytest.raises(VerificationError, match="invalid X.509 certificate"): utils.cert_is_ca(cert) @pytest.mark.parametrize( ("testcase", "valid"), [ ("bogus-root.pem", True), ("bogus-intermediate.pem", False), ("bogus-leaf.pem", False), ("bogus-leaf-invalid-ku.pem", False), ], ) def test_cert_is_root_ca(x509_testcase, testcase, valid): cert = x509_testcase(testcase) assert utils.cert_is_root_ca(cert) is valid @pytest.mark.parametrize( ("testcase", "valid"), ( ["bogus-root.pem", False], ["bogus-intermediate.pem", False], ["bogus-intermediate-with-eku.pem", False], ["bogus-leaf.pem", True], ["bogus-leaf-invalid-eku.pem", False], ), ) def test_cert_is_leaf(x509_testcase, testcase, valid): cert = x509_testcase(testcase) assert utils.cert_is_leaf(cert) is valid @pytest.mark.parametrize( "testcase", [ "bogus-root-invalid-ku.pem", "bogus-root-missing-ku.pem", "bogus-leaf-invalid-ku.pem", "bogus-leaf-missing-eku.pem", ], ) def test_cert_is_leaf_invalid_states(x509_testcase, testcase): cert = x509_testcase(testcase) with pytest.raises(VerificationError): utils.cert_is_leaf(cert) @pytest.mark.parametrize( "helper", [utils.cert_is_leaf, utils.cert_is_ca, utils.cert_is_root_ca] ) def test_cert_is_leaf_invalid_version(helper): cert = pretend.stub(version=x509.Version.v1) with pytest.raises(VerificationError, match="invalid X.509 version"): helper(cert) ================================================ FILE: test/unit/test_version.py ================================================ # 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. import sigstore def test_version(): assert isinstance(sigstore.__version__, str) ================================================ FILE: test/unit/verify/__init__.py ================================================ # 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. ================================================ FILE: test/unit/verify/test_policy.py ================================================ # 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. import re import pretend import pytest from cryptography.x509 import ExtensionNotFound from sigstore.errors import VerificationError from sigstore.verify import policy class TestVerificationPolicy: def test_does_not_init(self): with pytest.raises(TypeError, match="Can't instantiate abstract class"): policy.VerificationPolicy(pretend.stub()) class TestUnsafeNoOp: def test_succeeds(self, monkeypatch): logger = pretend.stub(warning=pretend.call_recorder(lambda s: None)) monkeypatch.setattr(policy, "_logger", logger) policy_ = policy.UnsafeNoOp() policy_.verify(pretend.stub()) assert logger.warning.calls == [ pretend.call( "unsafe (no-op) verification policy used! no verification performed!" ) ] class TestAnyOf: def test_trivially_false(self): policy_ = policy.AnyOf([]) with pytest.raises(VerificationError, match="0 of 0 policies succeeded"): policy_.verify(pretend.stub()) def test_fails_no_children_match(self, signing_bundle): _, bundle = signing_bundle("bundle.txt") policy_ = policy.AnyOf( [ policy.Identity(identity="foo", issuer="bar"), policy.Identity(identity="baz", issuer="quux"), ] ) with pytest.raises(VerificationError, match="0 of 2 policies succeeded"): policy_.verify(bundle.signing_certificate) def test_succeeds(self, signing_bundle): _, bundle = signing_bundle("bundle.txt") policy_ = policy.AnyOf( [ policy.Identity(identity="foo", issuer="bar"), policy.Identity(identity="baz", issuer="quux"), policy.Identity( identity="a@tny.town", issuer="https://github.com/login/oauth", ), ] ) policy_.verify(bundle.signing_certificate) class TestAllOf: def test_trivially_false(self): policy_ = policy.AllOf([]) with pytest.raises(VerificationError, match="no child policies to verify"): policy_.verify(pretend.stub()) def test_certificate_extension_not_found(self): policy_ = policy.AllOf([policy.Identity(identity="foo", issuer="bar")]) cert_ = pretend.stub( extensions=pretend.stub( get_extension_for_oid=pretend.raiser( ExtensionNotFound(oid=pretend.stub(), msg=pretend.stub()) ) ) ) reason = re.escape( "Certificate does not contain OIDCIssuer (1.3.6.1.4.1.57264.1.1) extension" ) with pytest.raises(VerificationError, match=reason): policy_.verify(cert_) def test_fails_not_all_children_match(self, signing_bundle): _, bundle = signing_bundle("bundle.txt") policy_ = policy.AllOf( [ policy.Identity(identity="foo", issuer="bar"), policy.Identity(identity="baz", issuer="quux"), policy.Identity( identity="a@tny.town", issuer="https://github.com/login/oauth", ), ] ) with pytest.raises( VerificationError, match="Certificate's OIDCIssuer does not match", ): policy_.verify(bundle.signing_certificate) def test_succeeds(self, signing_bundle): _, bundle = signing_bundle("bundle.txt") policy_ = policy.AllOf( [ policy.Identity( identity="a@tny.town", issuer="https://github.com/login/oauth", ), policy.Identity( identity="a@tny.town", issuer="https://github.com/login/oauth", ), ] ) policy_.verify(bundle.signing_certificate) class TestIdentity: def test_fails_no_san_match(self, signing_bundle): _, bundle = signing_bundle("bundle.txt") policy_ = policy.Identity( identity="bad@ident.example.com", issuer="https://github.com/login/oauth", ) with pytest.raises( VerificationError, match="Certificate's SANs do not match", ): policy_.verify(bundle.signing_certificate) class TestSingleExtPolicy: def test_succeeds(self, signing_bundle): _, bundle = signing_bundle("bundle_v3_github.whl") verification_policy_extensions = [ policy.OIDCIssuer("https://token.actions.githubusercontent.com"), policy.GitHubWorkflowTrigger("release"), policy.GitHubWorkflowSHA("d8b4a6445f38c48b9137a8099706d9b8073146e4"), policy.GitHubWorkflowName("release"), policy.GitHubWorkflowRepository("trailofbits/rfc8785.py"), policy.GitHubWorkflowRef("refs/tags/v0.1.2"), policy.OIDCIssuerV2("https://token.actions.githubusercontent.com"), policy.OIDCBuildSignerURI( "https://github.com/trailofbits/rfc8785.py/.github/workflows/release.yml@refs/tags/v0.1.2" ), policy.OIDCBuildSignerDigest("d8b4a6445f38c48b9137a8099706d9b8073146e4"), policy.OIDCRunnerEnvironment("github-hosted"), policy.OIDCSourceRepositoryURI("https://github.com/trailofbits/rfc8785.py"), policy.OIDCSourceRepositoryDigest( "d8b4a6445f38c48b9137a8099706d9b8073146e4" ), policy.OIDCSourceRepositoryRef("refs/tags/v0.1.2"), policy.OIDCSourceRepositoryIdentifier("768213997"), policy.OIDCSourceRepositoryOwnerURI("https://github.com/trailofbits"), policy.OIDCSourceRepositoryOwnerIdentifier("2314423"), policy.OIDCBuildConfigURI( "https://github.com/trailofbits/rfc8785.py/.github/workflows/release.yml@refs/tags/v0.1.2" ), policy.OIDCBuildConfigDigest("d8b4a6445f38c48b9137a8099706d9b8073146e4"), policy.OIDCBuildTrigger("release"), policy.OIDCRunInvocationURI( "https://github.com/trailofbits/rfc8785.py/actions/runs/8351058501/attempts/1" ), policy.OIDCSourceRepositoryVisibility("public"), ] policy_ = policy.AllOf(verification_policy_extensions) policy_.verify(bundle.signing_certificate) ================================================ FILE: test/unit/verify/test_verifier.py ================================================ # 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. import hashlib import json import logging from datetime import datetime, timezone import pretend import pytest import rfc3161_client from sigstore._internal.trust import CertificateAuthority from sigstore.dsse import StatementBuilder, Subject from sigstore.errors import VerificationError from sigstore.models import Bundle from sigstore.verify import policy from sigstore.verify.verifier import Verifier @pytest.mark.production def test_verifier_production(): verifier = Verifier.production() assert verifier is not None @pytest.mark.staging def test_verifier_staging(): verifier = Verifier.staging() assert verifier is not None @pytest.mark.staging def test_verifier_one_verification(signing_materials, null_policy): verifier = Verifier.staging() (file, bundle) = signing_materials("a.txt", verifier._rekor) verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.staging def test_verifier_inconsistent_log_entry(signing_bundle, null_policy): (file, bundle) = signing_bundle("bundle_cve_2022_36056.txt") verifier = Verifier.staging() with pytest.raises( VerificationError, match="transparency log entry is inconsistent with other materials", ): verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.staging def test_verifier_digest_mismatch(signing_bundle, null_policy): """The signature is over correct content, but digest documented in bundle is wrong""" (file, bundle) = signing_bundle("bundle.txt") bundle._inner.message_signature.message_digest.digest = b"" verifier = Verifier.staging() with pytest.raises( VerificationError, match="digest mismatch", ): verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.staging def test_verifier_multiple_verifications(signing_materials, null_policy): verifier = Verifier.staging() a = signing_materials("a.txt", verifier._rekor) b = signing_materials("b.txt", verifier._rekor) for file, bundle in [a, b]: verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.online @pytest.mark.parametrize( "filename", ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt", "staging-rekor-v2.txt"), ) def test_verifier_bundle_artifact(signing_bundle, null_policy, filename): (file, bundle) = signing_bundle(filename) verifier = Verifier.staging() verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.online @pytest.mark.parametrize( "filename", ("a.dsse.staging-rekor-v2.txt",), ) def test_verifier_bundle_dsse(signing_bundle, null_policy, filename): (file, bundle) = signing_bundle(filename) verifier = Verifier.staging() verifier.verify_dsse(bundle, null_policy) @pytest.mark.parametrize( "filename", ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt") ) def test_verifier_bundle_offline(signing_bundle, null_policy, filename): (file, bundle) = signing_bundle(filename) verifier = Verifier.staging(offline=True) verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.staging def test_verifier_email_identity(signing_materials): verifier = Verifier.staging() (file, bundle) = signing_materials("a.txt", verifier._rekor) policy_ = policy.Identity( identity="william@yossarian.net", issuer="https://github.com/login/oauth", ) verifier.verify_artifact( file.read_bytes(), bundle, policy_, ) @pytest.mark.staging def test_verifier_uri_identity(signing_materials): verifier = Verifier.staging() (file, bundle) = signing_materials("c.txt", verifier._rekor) policy_ = policy.Identity( identity=( "https://github.com/sigstore/" "sigstore-python/.github/workflows/ci.yml@refs/pull/288/merge" ), issuer="https://token.actions.githubusercontent.com", ) verifier.verify_artifact( file.read_bytes(), bundle, policy_, ) @pytest.mark.staging def test_verifier_policy_check(signing_materials): verifier = Verifier.staging() (file, bundle) = signing_materials("a.txt", verifier._rekor) # policy that fails to verify for any given cert. policy_ = pretend.stub(verify=pretend.raiser(VerificationError("policy failed"))) with pytest.raises(VerificationError, match="policy failed"): verifier.verify_artifact( file.read_bytes(), bundle, policy_, ) @pytest.mark.staging @pytest.mark.xfail def test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch): # FIXME(jl): can't mock: # - datetime.datetime.utcfromtimestamp: immutable type. # - entry.integrated_time: frozen dataclass. # - Certificate.not_valid_{before,after}: rust FFI. import datetime verifier = Verifier.staging() bundle: Bundle (file, bundle) = signing_materials("a.txt", verifier._rekor) entry = bundle._inner.verification_material.tlog_entries[0] entry.integrated_time = datetime.MINYEAR with pytest.raises(VerificationError): verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.staging @pytest.mark.ambient_oidc def test_verifier_dsse_roundtrip(staging): signer_cls, verifier_cls, identity = staging ctx = signer_cls() stmt = ( StatementBuilder() .subjects( [Subject(name="null", digest={"sha256": hashlib.sha256(b"").hexdigest()})] ) .predicate_type("https://cosign.sigstore.dev/attestation/v1") .predicate( { "Data": "", "Timestamp": "2023-12-07T00:37:58Z", } ) ).build() with ctx.signer(identity) as signer: bundle = signer.sign_dsse(stmt) verifier = verifier_cls() payload_type, payload = verifier.verify_dsse(bundle, policy.UnsafeNoOp()) assert payload_type == "application/vnd.in-toto+json" assert payload == stmt._contents class TestVerifierWithTimestamp: @pytest.fixture def verifier(self, asset) -> Verifier: """Returns a Verifier with Timestamp Authorities set.""" verifier = Verifier.staging(offline=True) authority = CertificateAuthority.from_json(asset("tsa/ca.json").as_posix()) verifier._trusted_root._inner.timestamp_authorities = [authority._inner] return verifier def test_verifier_verify_timestamp(self, verifier, asset, null_policy, monkeypatch): # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the # TSA timestamp are required monkeypatch.setattr("sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD", 2) verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()), null_policy, ) def test_verifier_no_validity_end(self, verifier, asset, null_policy): verifier._trusted_root.get_timestamp_authorities()[ 0 ]._inner.valid_for.end = None verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()), null_policy, ) @pytest.mark.parametrize( "fields_to_delete", ( [], ["inclusionPromise"], # integratedTime is required to verify the inclusionPromise. pytest.param(["integratedTime"], marks=pytest.mark.xfail), ["inclusionPromise", "integratedTime"], ), ) def test_verifier_verify_no_inclusion_promise_and_integrated_time( self, verifier, asset, null_policy, fields_to_delete ): """ Ensure that we can still verify a Bundle with an RFC 3161 timestamp if the SET isn't present. There is one exception: When inclusionPromise is present, but integratedTime is not, then we expect a failure because the integratedTime is required to verify the inclusionPromise. """ bundle_dict = json.loads(asset("tsa/bundle.txt.sigstore").read_bytes()) (entry_dict,) = bundle_dict["verificationMaterial"]["tlogEntries"] for field in fields_to_delete: del entry_dict[field] # Bundle.from_json() also validates the bundle's layout. bundle = Bundle.from_json(json.dumps(bundle_dict)) verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), bundle, null_policy, ) def test_verifier_without_timestamp( self, verifier, asset, null_policy, monkeypatch ): monkeypatch.setattr(verifier, "_establish_time", lambda *args: []) with pytest.raises(VerificationError, match="not enough sources"): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()), null_policy, ) def test_verifier_too_many_timestamp(self, verifier, asset, null_policy): with pytest.raises(VerificationError, match="too many"): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json( asset("tsa/bundle.many_timestamp.sigstore").read_bytes() ), null_policy, ) def test_verifier_duplicate_timestamp(self, verifier, asset, null_policy): with pytest.raises(VerificationError, match="duplicate"): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.duplicate.sigstore").read_bytes()), null_policy, ) def test_verifier_outside_validity_range( self, caplog, verifier, asset, null_policy, monkeypatch ): # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the # TSA timestamp are required monkeypatch.setattr("sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD", 2) # Set a date before the timestamp range verifier._trusted_root.get_timestamp_authorities()[ 0 ]._inner.valid_for.end = datetime(2024, 10, 31, tzinfo=timezone.utc) with caplog.at_level(logging.DEBUG, logger="sigstore.verify.verifier"): with pytest.raises( VerificationError, match="not enough sources of verified time" ): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()), null_policy, ) assert ( "Unable to verify Timestamp because not in CA time range." == caplog.records[0].message ) def test_verifier_rfc3161_error( self, verifier, asset, null_policy, caplog, monkeypatch ): # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the # TSA timestamp are required monkeypatch.setattr("sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD", 2) def verify_function(*args): raise rfc3161_client.VerificationError() monkeypatch.setattr(rfc3161_client.verify._Verifier, "verify", verify_function) with caplog.at_level(logging.DEBUG, logger="sigstore.verify.verifier"): with pytest.raises( VerificationError, match="not enough sources of verified time" ): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()), null_policy, ) assert caplog.records[0].message == "Unable to verify Timestamp with CA." def test_verifier_no_authorities(self, asset, null_policy): verifier = Verifier.staging(offline=True) verifier._trusted_root._inner.timestamp_authorities = [] with pytest.raises(VerificationError, match="no Timestamp Authorities"): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()), null_policy, ) def test_late_timestamp(self, caplog, verifier, asset, null_policy, monkeypatch): """ Ensures that verifying the signing certificate fails because the timestamp is outside the certificate's validity window. The sample bundle "tsa/bundle.txt.late_timestamp.sigstore" was generated by adding `time.sleep(12*60)` into `sigstore.sign.Signer._finalize_sign()`, just after the entry is posted to Rekor but before the timestamp is requested. """ # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the # TSA timestamp are required monkeypatch.setattr("sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD", 2) with pytest.raises( VerificationError, match="not enough sources of verified time" ): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json( asset("tsa/bundle.txt.late_timestamp.sigstore").read_bytes() ), null_policy, ) def test_verifier_not_enough_timestamp( self, verifier, asset, null_policy, monkeypatch ): # asset is a rekor v1 bundle: set threshold to 3 so integrated time and one # TSA timestamp are not enough monkeypatch.setattr("sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD", 3) with pytest.raises( VerificationError, match="not enough sources of verified time" ): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()), null_policy, ) def test_verify_signed_timestamp_regression(self, asset): """ Ensure we correctly verify a timestamp with no embedded certs. This is a regression test for # 1482 """ verifier = Verifier.staging(offline=True) ts = rfc3161_client.decode_timestamp_response( asset("tsa/issue1482-timestamp-with-no-cert").read_bytes() ) res = verifier._verify_signed_timestamp( ts, asset("tsa/issue1482-message").read_bytes() ) assert res is not None