[
  {
    "path": ".gitattributes",
    "content": "# These directories contain TUF and other assets that are either digested\n# or sized-checked so CRLF normalization breaks them.\nsigstore/_store/** binary diff=text\ntest/assets/** binary diff=text\ntest/assets/x509/** -binary\n"
  },
  {
    "path": ".github/actions/upload-coverage/action.yml",
    "content": "# Derived from <https://github.com/pyca/cryptography/blob/SOME_REF/.github/actions/upload-coverage/action.yml>\n# Originally authored by the PyCA Cryptography maintainers, and licensed under\n# the terms of the BSD license:\n# <https://github.com/pyca/cryptography/blob/main/LICENSE.BSD>\n\nname: Upload Coverage\ndescription: Upload coverage files\n\nruns:\n  using: \"composite\"\n\n  steps:\n    # FIXME(jl): codecov has the option of including machine information in filename that would solve this unique naming\n    # issue more completely.\n    - run: |\n        COVERAGE_UUID=$(python3 -c \"import uuid; print(uuid.uuid4())\")\n        echo \"COVERAGE_UUID=${COVERAGE_UUID}\" >> $GITHUB_OUTPUT\n        if [ -f .coverage ]; then\n          mv .coverage .coverage.${COVERAGE_UUID}\n        fi\n      id: coverage-uuid\n      shell: bash\n    - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n      with:\n        name: coverage-data-${{ steps.coverage-uuid.outputs.COVERAGE_UUID }}\n        include-hidden-files: 'true'\n        path: |\n          .coverage.*\n          *.lcov\n        if-no-files-found: ignore\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  - package-ecosystem: pip\n    directory: /\n    schedule:\n      interval: daily\n\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: daily\n    open-pull-requests-limit: 99\n    rebase-strategy: \"disabled\"\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: github-actions\n    directory: .github/actions/upload-coverage/\n    schedule:\n      interval: daily\n    open-pull-requests-limit: 99\n    rebase-strategy: \"disabled\"\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\nThanks for opening a pull request! Please do not just delete this text.  The three fields below are mandatory.\n\nPlease remember to:\n- This PR requires an issue. If it is a new feature, the issue should proceed the PR and will have allowed sufficient time for discussions to take place. Use\nissue tags such as \"Closes #XYZ\" or \"Resolves sigstore/repo-name#XYZ\".\n- add [documentation](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)\n- ensure your commits are signed-off, as sigstore uses the [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits\n\nThank you :)\n-->\n\n#### Summary\n<!--\n Explain the **motivation** for making this change. What existing problem does the pull request solve? How can reviewers test this PR?\n-->\n\n#### Release Note\n<!--\nAdd a release note for each of the following conditions in CHANGELOG.md:\n\n* Config changes (additions, deletions, updates)\n* API additions—new endpoint, new response fields, or newly accepted request parameters\n* Database changes (any)\n* Websocket additions or changes\n* Anything noteworthy to an administrator running private sigstore instances (err on the side of over-communicating)\n* New features and improvements, including behavioural changes, UI changes and CLI changes\n* Bug fixes and fixes of previous known issues\n* Deprecation warnings, breaking changes, or compatibility notes\n\nUse past-tense.\n\n-->\n\n#### Documentation\n<!--\n\nDoes this change require an update to documentation? How will users implement your new feature?\n\nPlease reference a PR within https://docs.sigstore.dev\n\n-->"
  },
  {
    "path": ".github/workflows/check-embedded-root.yml",
    "content": "name: Check embedded root\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '13 13 * * 3'\n\njobs:\n  check-embedded-root:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: Setup environment\n        run: make dev\n\n      - name: Check if embedded root is up-to-date\n        run: |\n          make update-embedded-root\n          git diff --exit-code\n\n\n      - if: failure()\n        name: Create an issue if embedded root is not up-to-date\n        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n        with:\n          script: |\n            const repo = context.repo.owner + \"/\" + context.repo.repo\n            const body = `\n            The Sigstore [TUF repository](https://tuf-repo-cdn.sigstore.dev/) contents have changed: the data embedded\n            in sigstore-python sources can be updated. This is not urgent but will improve cold-cache performance.\n            \n            Run \\`make update-embedded-root\\` to update the embedded data.\n            \n            This issue was filed by _${context.workflow}_ [workflow run](${context.serverUrl}/${repo}/actions/runs/${context.runId}).\n            `\n\n            const issues = await github.rest.search.issuesAndPullRequests({\n              q: \"label:embedded-root-update+state:open+type:issue+repo:\" + repo,\n            })\n            if (issues.data.total_count > 0) {\n              console.log(\"Issue for embedded root update exists already.\")\n            } else {\n              github.rest.issues.create({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                title: \"Embedded TUF root is not up-to-date\",\n                labels: [\"embedded-root-update\"],\n                body: body,\n              })\n              console.log(\"New issue created.\")\n            }\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n      - series/*\n  pull_request:\n  schedule:\n    - cron: \"0 11 * * *\"\n  workflow_dispatch:\n\npermissions: {}\n\njobs:\n  test:\n    # Avoid scheduled runs in forks\n    if: github.event_name != 'schedule' || github.repository == 'sigstore/sigstore-python'\n    permissions:\n      # Needed to access the workflow's OIDC identity.\n      id-token: write\n    strategy:\n      matrix:\n        conf:\n          - { py: \"3.10\", os: \"ubuntu-latest\" }\n          - { py: \"3.11\", os: \"ubuntu-latest\" }\n          - { py: \"3.12\", os: \"ubuntu-latest\" }\n          - { py: \"3.13\", os: \"ubuntu-latest\" }\n          - { py: \"3.14\", os: \"ubuntu-latest\" }\n          # NOTE: We only test Windows and macOS on the latest Python;\n          # these primarily exist to ensure that we don't accidentally\n          # introduce Linux-isms into the development tooling.\n          - { py: \"3.14\", os: \"windows-latest\" }\n          - { py: \"3.14\", os: \"macos-latest\" }\n    runs-on: ${{ matrix.conf.os }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: ${{ matrix.conf.py }}\n          allow-prereleases: true\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: deps\n        run: make dev SIGSTORE_EXTRA=test\n\n      - name: test (offline)\n        if: matrix.conf.os == 'ubuntu-latest'\n        run: |\n          # Look at me. I am the captain now.\n          sudo sysctl -w kernel.unprivileged_userns_clone=1\n          sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n\n          # We use `unshare` to \"un-share\" the default networking namespace,\n          # in effect running the tests as if the host is offline.\n          # This in turn effectively exercises the correctness of our\n          # \"online-only\" test markers, since any test that's online\n          # but not marked as such will fail.\n          # We also explicitly exclude the integration tests, since these are\n          # always online.\n          unshare --map-root-user --net make test T=\"test/unit\" TEST_ARGS=\"--skip-online -vv --showlocals\"\n\n      - name: test\n        run: make test TEST_ARGS=\"-vv --showlocals\"\n\n      # TODO: Refactor this or remove it entirely once there's\n      # a suitable staging TSA instance.\n      - name: test (timestamp-authority)\n        if: ${{ matrix.conf.os == 'ubuntu-latest' }}\n        run: |\n          # Fetch the latest sigstore/timestamp-authority build\n          SIGSTORE_TIMESTAMP_VERSION=$(gh api /repos/sigstore/timestamp-authority/releases --jq '.[0].tag_name')\n          wget https://github.com/sigstore/timestamp-authority/releases/download/${SIGSTORE_TIMESTAMP_VERSION}/timestamp-server-linux-amd64 -O /tmp/timestamp-server\n          chmod +x /tmp/timestamp-server\n\n          # Run the TSA in background\n          /tmp/timestamp-server serve --port 3000 --disable-ntp-monitoring &\n          export TEST_SIGSTORE_TIMESTAMP_AUTHORITY_URL=\"http://localhost:3000/api/v1/timestamp\"\n\n          # Ensure Timestamp Authority tests are not skipped by\n          # having pytest show skipped tests and verifying ours are running\n          set -o pipefail\n          make test TEST_ARGS=\"-m timestamp_authority -rs\" | tee output\n          ! grep -q \"skipping test that requires a Timestamp Authority\" output || (echo \"ERROR: Found skip message\" && exit 1)\n        env:\n          # Needed for `gh api` above.\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: test (interactive)\n        if: (github.event_name != 'pull_request') || !github.event.pull_request.head.repo.fork\n        run: make test-interactive TEST_ARGS=\"-vv --showlocals\"\n\n      - uses: ./.github/actions/upload-coverage\n        # only aggregate test coverage over linux-based tests to avoid any OS-specific filesystem information stored in\n        # coverage metadata.\n        if: ${{ matrix.conf.os == 'ubuntu-latest' }}\n\n  all-tests-pass:\n    if: always() && (github.event_name != 'schedule' || github.repository == 'sigstore/sigstore-python')\n\n    needs:\n      - test\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: check test jobs\n        uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2\n        with:\n          jobs: ${{ toJSON(needs) }}\n\n  coverage:\n    needs:\n      - test\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n\n      - run: pip install coverage[toml]\n\n      - name: download coverage data\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          path: all-artifacts/\n\n      - name: combine coverage data\n        id: combinecoverage\n        run: |\n          set +e\n          python -m coverage combine all-artifacts/coverage-data-*\n          echo \"## python coverage\" >> $GITHUB_STEP_SUMMARY\n          python -m coverage report -m --format=markdown >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".github/workflows/conformance.yml",
    "content": "name: Conformance Tests\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n  pull_request:\n  schedule:\n    - cron: \"45 7 * * 1,4\"\n\npermissions: {}\n\njobs:\n  conformance:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: install sigstore-python\n        run: python -m pip install .\n\n      - uses: sigstore/sigstore-conformance@9611941d54398f2e3f6383b6f744442a56d2fb2a # v0.0.26\n        with:\n          entrypoint: ${{ github.workspace }}/test/integration/sigstore-python-conformance\n          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\n\n  file-issue-on-failure:\n    needs: [conformance]\n    if: failure() && github.event_name == 'schedule' && github.repository == 'sigstore/sigstore-python'\n    permissions:\n      issues: write # required to file an issue\n    runs-on: ubuntu-latest\n    steps:\n      - name: File an issue for conformance test failure\n        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            github.rest.issues.create({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              title: `[CI] Scheduled conformance test failed`,\n              body: `\n            A scheduled conformance test failed, see [run details](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).\n            `\n            });\n"
  },
  {
    "path": ".github/workflows/cross-os.yml",
    "content": "# Copyright 2025 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Cross-platform sign and verify\non:\n  push:\n    branches:\n      - main\n      - series/*\n  pull_request:\n  workflow_dispatch:\n\npermissions: {}\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  sign:\n    name: Sign on ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu, macos, windows]\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n      - run: pip install .\n      - name: Fetch testing oidc token\n        uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@4a8befcc16064dac9e97f210948d226e5c869bdc # v1.0.0\n      - name: Sign \n        run: python -m sigstore --staging sign --identity-token $(cat oidc-token.txt) test/assets/a.txt\n      - name: upload signature bundle\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: ${{ matrix.os }}-bundle\n          path: test/assets/a.txt.sigstore.json\n          if-no-files-found: error\n          retention-days: 1\n  verify:\n    name: Verify ${{ matrix.signed-with-os }} bundle on ${{ matrix.os }}\n    if: ${{ always() }} # don't stop some verification if one of the signing jobs failed\n    needs: [sign]\n    runs-on: ${{ matrix.os }}-latest\n    strategy:\n      fail-fast: false # Don't cancel other jobs if one fails\n      matrix:\n        os: [ubuntu, macos, windows]\n        signed-with-os: [ubuntu, macos, windows]\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n      - run: pip install .\n      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: ${{ matrix.signed-with-os }}-bundle\n      - name: Verify\n        run: |\n          python -m sigstore --staging verify github --verbose \\\n            --cert-identity \"https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main\" \\\n            --bundle a.txt.sigstore.json \\\n            test/assets/a.txt\n"
  },
  {
    "path": ".github/workflows/cross-version-verify.yaml",
    "content": "# Copyright 2025 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Cross-version verify\non:\n  push:\n    branches:\n      - main\n      - series/*\n  pull_request:\n  workflow_dispatch:\n\npermissions: {}\n\njobs:\n  sign:\n    name: Sign\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n      - run: pip install .\n      - name: Fetch testing oidc token\n        uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@4a8befcc16064dac9e97f210948d226e5c869bdc # v1.0.0\n      - name: Sign \n        run: |\n          touch artifact\n          python -m sigstore --staging sign --bundle artifact-staging-rekor2.sigstore.json --identity-token $(cat oidc-token.txt) --rekor-version=2 artifact\n          python -m sigstore --staging sign --bundle artifact-staging-rekor1.sigstore.json --identity-token $(cat oidc-token.txt) --rekor-version=1 artifact\n          python -m sigstore sign --bundle artifact-prod-rekor1.sigstore.json --identity-token $(cat oidc-token.txt) --rekor-version=1 artifact\n      - name: upload signature bundle\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: bundle\n          path: artifact*.sigstore.json\n          if-no-files-found: error\n          retention-days: 1\n  verify:\n    name: Verify with ${{ matrix.version }} on ${{ matrix.env }}\n    needs: [sign]\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false # Don't cancel other jobs if one fails\n      matrix:\n        # hand crafted list of old versions we care about\n        version: [3.5.6, 3.6.7, 4.0.0, 4.1.0, 4.2.0]\n        env: [staging, prod]\n        exclude:\n          # exclude staging for versions with https://github.com/sigstore/sigstore-python/issues/1656\n          - env: staging\n            version: 3.5.6\n          - env: staging\n            version: 4.0.0\n          - env: staging\n            version: 4.1.0\n\n    steps:\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n      - run: pip install sigstore==${{ matrix.version }}\n      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: bundle\n      - run: touch artifact\n      - name: Verify (Rekor v2)\n        # Rekor v2 is currently only available on staging, and only supported on sigstore-python 4.x\n        if: startsWith(matrix.version, '3.') != true && matrix.env == 'staging'\n        env:\n          ENV_OPT: ${{ matrix.env == 'staging' && '--staging' || '' }}\n          BUNDLE: artifact-${{matrix.env}}-rekor2.sigstore.json\n        run: |\n          python -m sigstore $ENV_OPT verify github --verbose \\\n            --cert-identity \"https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main\" \\\n            --bundle $BUNDLE \\\n            artifact\n      - name: Verify (Rekor v1)\n        env:\n          ENV_OPT: ${{ matrix.env == 'staging' && '--staging' || '' }}\n          BUNDLE: artifact-${{matrix.env}}-rekor1.sigstore.json\n        run: |\n          python -m sigstore $ENV_OPT verify github --verbose \\\n            --cert-identity \"https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main\" \\\n            --bundle $BUNDLE \\\n            artifact\n"
  },
  {
    "path": ".github/workflows/depsreview.yml",
    "content": "#\n# Copyright 2022 The Sigstore Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nname: 'Dependency Review'\non: [pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  dependency-review:\n    name: License and Vulnerability Scan\n    uses: sigstore/community/.github/workflows/reusable-dependency-review.yml@9b1b5aca605f92ec5b1bf3681b1e61b3dbc420cc\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Documentation\n\non:\n  push:\n    branches:\n      - main\n\npermissions: {}\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: setup\n        run: |\n          make dev SIGSTORE_EXTRA=doc\n\n      - name: build docs\n        run: |\n          make doc\n\n      - name: upload docs artifact\n        uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0\n        with:\n          path: ./html/\n\n  # This is copied from the official `pdoc` example:\n  #   https://github.com/mitmproxy/pdoc/blob/main/.github/workflows/docs.yml\n  #\n  # Deploy the artifact to GitHub pages.\n  # This is a separate job so that only actions/deploy-pages has the necessary permissions.\n  deploy:\n    needs: build\n    if: github.repository == 'sigstore/sigstore-python'\n    runs-on: ubuntu-latest\n    permissions:\n      # NOTE: Needed to push to the repository.\n      pages: write\n      id-token: write\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - id: deployment\n        uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\npermissions: {}\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: deps\n        run: make dev SIGSTORE_EXTRA=lint\n\n      - name: lint\n        run: make lint\n\n  check-readme:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      # NOTE: We intentionally check --help rendering against our minimum Python,\n      # since it changes slightly between Python versions.\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.10\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: deps\n        run: make dev\n\n      - name: check-readme\n        run: make check-readme\n\n  licenses:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      # adapted from Warehouse's bin/licenses\n      - run: |\n          for fn in $(find . -type f -name \"*.py\"); do\n            if [[ ! \"$(head -5 $fn | grep \"^ *\\(#\\|\\*\\|\\/\\/\\) .* License\\(d*\\)\")\" ]]; then\n              echo \"${fn} is missing a license\"\n              exit 1\n            fi\n          done\n\n  x509-testcases:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      # NOTE: We intentionally check test certificates against our minimum supported Python.\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.10\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: deps\n        run: make dev\n\n      - name: ensure testcase generation does not regress\n        run: make gen-x509-testcases\n\n  all-lints-pass:\n    if: always()\n\n    needs:\n      - lint\n      - check-readme\n      - licenses\n      - x509-testcases\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: check lint jobs\n        uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2\n        with:\n          jobs: ${{ toJSON(needs) }}\n"
  },
  {
    "path": ".github/workflows/pin-requirements.yml",
    "content": "name: Pin Requirements\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: Tag to pin dependencies against.\n        required: false\n        type: string\n\n  workflow_call:\n    inputs:\n      tag:\n        description: Tag to pin dependencies against.\n        required: false\n        type: string\n\npermissions:\n  contents: read\n\njobs:\n  update-pinned-requirements:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write # Branch creation for PR.\n\n    outputs:\n      sigstore-release-tag: ${{ steps.get-branch.outputs.sigstore-release-tag }}\n      sigstore-pin-requirements-branch: ${{ steps.get-branch.outputs.sigstore-pin-requirements-branch }}\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: main\n          # NOTE: Needed for `git describe` below.\n          fetch-depth: 0\n          fetch-tags: true\n          # NOTE: Needed to push back to the repo.\n          persist-credentials: true\n\n      - name: Get latest tag\n        run: |\n          latest_tag=$(git describe --tags --abbrev=0)\n          echo \"LATEST_TAG=${latest_tag}\" >> \"${GITHUB_ENV}\"\n\n      - name: Set SIGSTORE_RELEASE_TAG and SIGSTORE_NEW_BRANCH\n        id: get-branch\n        env:\n          INPUT_TAG: \"${{ inputs.tag }}\"\n        run: |\n          if [[ -n \"${INPUT_TAG}\" ]]; then\n            effective_tag=\"${INPUT_TAG}\"\n          else\n            effective_tag=\"${LATEST_TAG}\"\n          fi\n\n          # Environment\n          echo \"SIGSTORE_RELEASE_TAG=${effective_tag}\" >> \"${GITHUB_ENV}\"\n          echo \"SIGSTORE_NEW_BRANCH=pin-requirements/sigstore/${effective_tag}\" >> \"${GITHUB_ENV}\"\n\n          # Outputs\n          echo \"sigstore-release-tag=${effective_tag}\" >> \"${GITHUB_OUTPUT}\"\n          echo \"sigstore-pin-requirements-branch=pin-requirements/sigstore/${effective_tag}\" >> \"${GITHUB_OUTPUT}\"\n\n      - name: Configure git\n        run: |\n          # Set up committer info.\n          # https://github.com/orgs/community/discussions/26560\n          git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n          git config user.name \"github-actions[bot]\"\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version-file: install/.python-version\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: Install dependencies\n        run: pip install pip-tools\n\n      - name: Compute version from tag\n        run: |\n          echo \"SIGSTORE_RELEASE_VERSION=$(echo \"${SIGSTORE_RELEASE_TAG}\" | sed 's/^v//')\" >> \"${GITHUB_ENV}\"\n\n      - name: Update requirements\n        run: |\n          cd install\n\n          echo \"sigstore==${SIGSTORE_RELEASE_VERSION}\" > requirements.in\n          pip-compile --allow-unsafe --generate-hashes --upgrade --output-file=requirements.txt requirements.in\n\n      - name: Commit changes and push to branch\n        run: |\n          git commit --all -s -m \"[BOT] install: update pinned requirements\"\n          git push -f origin \"main:${SIGSTORE_NEW_BRANCH}\"\n\n  test-requirements:\n    needs: update-pinned-requirements\n    uses: ./.github/workflows/requirements.yml\n    with:\n      # We can't use `env` variables in this context.\n      # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability\n      ref: ${{ needs.update-pinned-requirements.outputs.sigstore-pin-requirements-branch }}\n\n  create-pr:\n    needs:\n      - update-pinned-requirements\n      - test-requirements\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write # Pull Request branch modification.\n      pull-requests: write # Pull Request creation.\n\n    env:\n      SIGSTORE_RELEASE_TAG: ${{ needs.update-pinned-requirements.outputs.sigstore-release-tag }}\n      SIGSTORE_PIN_REQUIREMENTS_BRANCH: ${{ needs.update-pinned-requirements.outputs.sigstore-pin-requirements-branch }}\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ env.SIGSTORE_PIN_REQUIREMENTS_BRANCH }}\n          # NOTE: Needed to push back to the repo.\n          persist-credentials: true\n\n      - name: Reset remote PR branch\n        run: |\n          git fetch origin main\n          git push -f origin \"origin/main:${SIGSTORE_PIN_REQUIREMENTS_BRANCH}\"\n\n      - name: Open pull request\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          # Try to create a new PR, or update if it already exists\n          # NOTE: Branch deletion after merge is handled by repository settings\n          PR_TITLE=\"Update pinned requirements for ${SIGSTORE_RELEASE_TAG}\"\n          PR_BODY=\"Pins dependencies for <https://github.com/sigstore/sigstore-python/releases/tag/${SIGSTORE_RELEASE_TAG}>.\"\n          \n          if ! gh pr create \\\n            --title \"${PR_TITLE}\" \\\n            --body \"${PR_BODY}\" \\\n            --base main \\\n            --head \"${SIGSTORE_PIN_REQUIREMENTS_BRANCH}\"; then\n            # PR already exists, update it\n            gh pr edit \"${SIGSTORE_PIN_REQUIREMENTS_BRANCH}\" \\\n              --title \"${PR_TITLE}\" \\\n              --body \"${PR_BODY}\"\n          fi\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types:\n      - published\n\npermissions: {}\n\njobs:\n  build:\n    name: Build and sign artifacts\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          # NOTE: We intentionally don't use a cache in the release step,\n          # to reduce the risk of cache poisoning.\n          python-version: \"3.x\"\n\n      - name: deps\n        run: python -m pip install -U build\n\n      - name: build\n        run: python -m build\n\n      - name: sign\n        run: |\n          mkdir -p smoketest-artifacts\n\n          # we smoke-test sigstore by installing each of the distributions\n          # we've built in a fresh environment and using each to sign and\n          # verify for itself, using the ambient OIDC identity\n          for dist in dist/*; do\n            dist_base=\"$(basename \"${dist}\")\"\n\n            python -m venv smoketest-env\n\n            ./smoketest-env/bin/python -m pip install \"${dist}\"\n\n            # NOTE: signing artifacts currently go in a separate directory,\n            # to avoid confusing the package uploader (which otherwise tries\n            # to upload them to PyPI and fails). Future versions of twine\n            # and the gh-action-pypi-publish action should support these artifacts.\n            ./smoketest-env/bin/python -m \\\n              sigstore sign \"${dist}\" \\\n              --output-signature smoketest-artifacts/\"${dist_base}.sig\" \\\n              --output-certificate smoketest-artifacts/\"${dist_base}.crt\" \\\n              --bundle smoketest-artifacts/\"${dist_base}.sigstore\"\n\n            # Verify using `.sig` `.crt` pair;\n            ./smoketest-env/bin/python -m \\\n              sigstore verify identity \"${dist}\" \\\n              --signature \"smoketest-artifacts/${dist_base}.sig\" \\\n              --cert \"smoketest-artifacts/${dist_base}.crt\" \\\n              --cert-oidc-issuer https://token.actions.githubusercontent.com \\\n              --cert-identity ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/release.yml@${GITHUB_REF}\n\n            # Verify using `.sigstore` bundle;\n            ./smoketest-env/bin/python -m \\\n              sigstore verify identity \"${dist}\" \\\n              --bundle \"smoketest-artifacts/${dist_base}.sigstore\" \\\n              --cert-oidc-issuer https://token.actions.githubusercontent.com \\\n              --cert-identity ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/release.yml@${GITHUB_REF}\n\n            rm -rf smoketest-env\n          done\n\n      - name: Upload built packages\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: built-packages\n          path: ./dist/\n          if-no-files-found: warn\n\n      - name: Upload smoketest-artifacts\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: smoketest-artifacts\n          path: smoketest-artifacts/\n          if-no-files-found: warn\n\n  generate-provenance:\n    needs: [build]\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write # To sign the provenance.\n      attestations: write # To persist the attestation files.\n    steps:\n      - name: Download artifacts directories # goes to current working directory\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      - name: Generate build provenance\n        uses: actions/attest-build-provenance@v4\n        with:\n          subject-path: \"built-packages/*\"\n\n  release-pypi:\n    needs: [build, generate-provenance]\n    runs-on: ubuntu-latest\n    permissions:\n      # Used to authenticate to PyPI via OIDC.\n      id-token: write\n    steps:\n      - name: Download artifacts directories # goes to current working directory\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n\n      - name: publish\n        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0\n        with:\n          packages-dir: built-packages/\n\n  release-github:\n    needs: [build, generate-provenance]\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload release assets.\n      contents: write\n    steps:\n      - name: Download artifacts directories # goes to current working directory\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n\n      - name: Upload artifacts to github\n        # Confusingly, this action also supports updating releases, not\n        # just creating them. This is what we want here, since we've manually\n        # created the release that triggered the action.\n        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0\n        with:\n          # smoketest-artifacts/ contains the signatures and certificates.\n          files: |\n            built-packages/*\n\n  # Trigger workflow to generate pinned requirements.txt.\n  pin-requirements:\n    permissions:\n      # Needed to create branch and pull request.\n      pull-requests: write\n      contents: write\n    # Workflow depends on uploaded release assets.\n    needs: [release-github]\n    # Only trigger workflow on full releases.\n    if: ${{ !github.event.release.prerelease }}\n    uses: ./.github/workflows/pin-requirements.yml\n    with:\n      tag: ${{ github.ref_name }}\n"
  },
  {
    "path": ".github/workflows/requirements.yml",
    "content": "name: Test requirements.txt\n\non:\n  push:\n    branches:\n      - main\n  workflow_call:\n    inputs:\n      ref:\n        description: The branch, tag, or revision to test.\n        type: string\n        required: true\n  pull_request:\n  schedule:\n    - cron: \"0 12 * * *\"\n\npermissions: {}\n\njobs:\n  test_requirements:\n    name: requirements.txt / ${{ matrix.python_version }}\n    runs-on: ubuntu-latest\n\n    env:\n      SIGSTORE_REF: ${{ inputs.ref }}\n    strategy:\n      matrix:\n        python_version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n    steps:\n      - name: Populate reference from context\n        if: ${{ env.SIGSTORE_REF == '' }}\n        run: |\n          echo \"SIGSTORE_REF=${GITHUB_REF}\" >> \"${GITHUB_ENV}\"\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ env.SIGSTORE_REF }}\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        name: Install Python ${{ matrix.python_version }}\n        with:\n          python-version: ${{ matrix.python_version }}\n          allow-prereleases: true\n          cache: \"pip\"\n\n      - name: Run test install\n        run: python -m pip install --require-hashes --no-deps -r install/requirements.txt\n"
  },
  {
    "path": ".github/workflows/scorecards-analysis.yml",
    "content": "name: Scorecards supply-chain security\non:\n  # Only the default branch is supported.\n  workflow_dispatch: # Manual\n  branch_protection_rule:\n  schedule:\n    - cron: '30 4 * * 0'\n  push:\n    branches: [ main ]\n\n# Clear default permissions.\npermissions: {}\n\njobs:\n  analysis:\n    name: Scorecards analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      actions: read\n      contents: read\n      # Needed to access GitHub's OIDC token which ensures the uploaded results integrity.\n      id-token: write\n    steps:\n      - name: \"Checkout code\"\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: \"Run analysis\"\n        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3\n        with:\n          results_file: results.sarif\n          results_format: sarif\n          # Read-only PAT token. To create it,\n          # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.\n          repo_token: ${{ secrets.SCORECARD_TOKEN }}\n          # Publish the results to enable scorecard badges. For more details, see\n          # https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories, `publish_results` will automatically be set to `false`,\n          # regardless of the value entered here.\n          publish_results: true\n\n      # Upload the results as artifacts (optional).\n      - name: \"Upload artifact\"\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: \"Upload to code-scanning\"\n        uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/staging-tests.yml",
    "content": "name: Staging Instance Tests\n\non:\n  push:\n    branches:\n      - main\n  schedule:\n    - cron: \"0 */8 * * *\"\n\npermissions: {}\n\njobs:\n  staging-tests:\n    if: github.event_name != 'schedule' || github.repository == 'sigstore/sigstore-python'\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to access the workflow's OIDC identity.\n      id-token: write\n\n      # Needed to create an issue, on failure.\n      issues: write\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: \"3.x\"\n          cache: \"pip\"\n          cache-dependency-path: pyproject.toml\n\n      - name: staging tests\n        env:\n          SIGSTORE_LOGLEVEL: DEBUG\n        run: |\n          # This is similar to the \"smoketest\" that we run during the\n          # release workflow, except that we run against Sigstore's\n          # staging instances instead.\n          # We also don't bother to build distributions.\n\n          python -m venv staging-env\n\n          ./staging-env/bin/python -m pip install .\n\n          # Our signing target is not important here, so we just sign\n          # the README in the repository.\n          ./staging-env/bin/python -m sigstore --verbose --staging sign README.md\n\n          # Verification also requires a different Rekor instance, so we\n          # also test it.\n          ./staging-env/bin/python -m sigstore --verbose --staging verify identity \\\n            --cert-oidc-issuer https://token.actions.githubusercontent.com \\\n            --cert-identity ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/staging-tests.yml@${GITHUB_REF} \\\n            README.md\n\n      - name: generate an issue if staging tests fail\n        if: failure()\n        run: |\n          cat <<- EOF > /tmp/staging-instance-issue.md\n          ## Staging instance failure\n\n          A scheduled test against Sigstore's staging instance has failed.\n\n          This suggests one of three conditions:\n\n          * A backwards-incompatible change in a Sigstore component;\n          * A regression in \\`sigstore-python\\`;\n          * A transient error.\n\n          The full CI failure can be found here:\n\n          ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/$GITHUB_RUN_ID\n          EOF\n\n      - name: open an issue if the staging tests fail\n        if: failure()\n        uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0\n        with:\n          title: \"[CI] Integration failure: staging instance\"\n          # created in the previous step\n          content-filepath: /tmp/staging-instance-issue.md\n          labels: bug,component:cicd,component:tests\n          assignees: woodruffw,di,tetsuo-cpp\n"
  },
  {
    "path": ".gitignore",
    "content": ".cache/\nenv/\npip-wheel-metadata/\n*.egg-info/\n__pycache__/\n.coverage*\nhtml/\ndist/\n.python-version\nbuild\n\n# Ignore some file types that may litter the root directory\n*.txt\n*.crt\n*.sig\n*.pem\n*.sh\n*.pub\n*.rekor\n*.sigstore\n*.sigstore.json\n\n# Don't ignore these files when we intend to include them\n!sigstore/_store/*.crt\n!sigstore/_store/*.pem\n!sigstore/_store/*.pub\n!test/assets/**\n!test/assets/staging-tuf/**\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to `sigstore-python` will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\nAll versions prior to 0.9.0 are untracked.\n\n## [Unreleased]\n\n### Fixed\n\n* Fixed ~60s hang after completing browser-based OIDC authentication.\n  The OIDC redirect server had incomplete HTTP responses and no connection\n  management, causing a keep-alive deadlock with the browser.\n\n## [4.2.0]\n\n### Fixed\n\n* Add state validation to OIDC flow to prevent Cross-site request forgery\n  during OIDC authorization\n  ([GHSA-hm8f-75xx-w2vr](https://github.com/sigstore/sigstore-python/security/advisories/GHSA-hm8f-75xx-w2vr))\n* verification now ensures that artifact digest documented in bundle and the real digest match\n  (this is a bundle consistency check: bundle signature was always verified over real digest)\n  ([#1652](https://github.com/sigstore/sigstore-python/pull/1652))\n* Fix issue with Signed Certificate Timestamp parsing where extensions\n  were not allowed by sigstore-python\n  ([1657](https://github.com/sigstore/sigstore-python/pull/1657), [1659](https://github.com/sigstore/sigstore-python/pull/1659))\n\n### Changed\n\n* Update supported public key algorithms\n  ([#1604](https://github.com/sigstore/sigstore-python/pull/1604))\n* trust: Update embedded TUF root\n  ([#1589](https://github.com/sigstore/sigstore-python/pull/1589))\n\n### Removed\n\n* Removed support for Python 3.9 as it is end-of-life\n  ([#1645](https://github.com/sigstore/sigstore-python/pull/1645))\n* Removed unused nonce in Oauth flow\n  ([#1649](https://github.com/sigstore/sigstore-python/pull/1649))\n\n\n## [4.1.0]\n\n### Added\n\n* cli: Support using other Sigstore instances with `--instance URL`.\n  New instances are trusted with new top level command `trust-instance ROOTFILE`.\n  [#1548](https://github.com/sigstore/sigstore-python/pull/1548)\n\n### Changed\n\n* Added cryptography 46 to list of compatible cryptography releases\n  ([#1544](https://github.com/sigstore/sigstore-python/pull/1544))\n* Improved error message when verifying bundles with unsupported log entry versions\n  ([#1569](https://github.com/sigstore/sigstore-python/pull/1569))\n\n### Fixed\n\n* cli: Always read/write UTF-8. This fixes an issue on Windows where the platform\n  default encoding was used: the issue has existed for a while, but became more visible\n  with signature bundles that contain rekor2 entries.\n  [#1553](https://github.com/sigstore/sigstore-python/pull/1553)\n\n## [4.0.0]\n\nThis is a major release with a host of API and functionality changes. The major new feature\nis Rekor v2 support but many other changes are also included, see list below.\n\n### Added\n\n* cli: Add `--rekor-version` to `sign` command arguments: This can be useful\n  if Sigstore instance provides multiple Rekor versions and user wants to\n  override the default choice\n  [#1471](https://github.com/sigstore/sigstore-python/pull/1471)\n* cli: Support parallel signing. When multiple artifacts are signed, the Rekor\n  requests are submitted in parallel: this is especially useful with Rekor v2.\n  [#1468](https://github.com/sigstore/sigstore-python/pull/1468), [#1478](https://github.com/sigstore/sigstore-python/pull/1478),\n  [#1485](https://github.com/sigstore/sigstore-python/pull/1485)\n* oidc (API): Allow custom audience claims via API\n  [#1402](https://github.com/sigstore/sigstore-python/pull/1402)\n* rekor (API): Support Rekor v2 (aka rekor-tiles) in both verification and signing.\n  [#1370](https://github.com/sigstore/sigstore-python/pull/1370), [#1422](https://github.com/sigstore/sigstore-python/pull/1422),\n  [#1432](https://github.com/sigstore/sigstore-python/pull/1432)\n* trust (API): Make TrustedRoot, SigningConfig and ClientTrustConfig public API\n  [#1496](https://github.com/sigstore/sigstore-python/pull/1496)\n\n### Changed\n\n* cli: Improve verify UX when wrong instance is used\n  [#1510](https://github.com/sigstore/sigstore-python/pull/1510)\n* deps: replace sigstore_protobuf_specs dependency with sigstore-models\n  [#1470](https://github.com/sigstore/sigstore-python/pull/1470)\n* trust: Update embedded TUF root\n  [#1515](https://github.com/sigstore/sigstore-python/pull/1515)\n* trust (API): TrustConfig now provides the `production()`and `staging()` helpers. Similar methods were removed from\n  SigningConfig, TrustedRoot, SigningContext and Issuer. Use TrustConfig everywhere in code base.\n  [#1363](https://github.com/sigstore/sigstore-python/pull/1363)\n* trust (API): support SigningConfig v0.2, remove support for v0.1. The new format now fully defines the\n  sigstore instance the client uses. `SigningConfig` class now has methods to return actual clients\n  (like RekorClient) instead of just URLs for that sigstore instance. The `--trust-config` cli option now\n  expects the trust config to contain a v0.2 SigningConfig.\n  [#1358](https://github.com/sigstore/sigstore-python/pull/1358), [#1407](https://github.com/sigstore/sigstore-python/pull/1407)\n* trust: Support ed25519 keys in trusted root\n  [#1377](https://github.com/sigstore/sigstore-python/pull/1377)\n\n### Fixed\n\n* rekor: resolve circular import of LogEntry\n  [#1458](https://github.com/sigstore/sigstore-python/pull/1458)\n* rekor: Fix checkpoint signature lookup when there are multiple signatures\n  [#1514](https://github.com/sigstore/sigstore-python/pull/1514)\n* rekor: Fix entry handling so inclusion promise is optional\n  [#1382](https://github.com/sigstore/sigstore-python/pull/1382)\n* rekor: Avoid trailing slash in post to /entries\n  [#1366](https://github.com/sigstore/sigstore-python/pull/1366)\n* sign: fetch TSA timestamps before submitting an entry to Rekor\n  [#1463](https://github.com/sigstore/sigstore-python/pull/1463)\n* timestamp: Specify sha256 in TSA timestamp request\n  [#1373](https://github.com/sigstore/sigstore-python/pull/1373)\n* trust: Fail less hard when trusted root contains unknown keys\n  [#1424](https://github.com/sigstore/sigstore-python/pull/1424)\n* verify: Fix TSA cert chain construction (fixes issue in the case where certificate is not embedded in\n  the timestamp)\n  [#1482](https://github.com/sigstore/sigstore-python/pull/1482)\n* verify: Use TSA hash algorithm specified in the timestamp (SHA-256, SHA-384 and SHA-512 are supported)\n  [#1385](https://github.com/sigstore/sigstore-python/pull/1385)\n* verify: Check artifact signing time against all established times\n  [#1381](https://github.com/sigstore/sigstore-python/pull/1381)\n* verify: Handle unset TSA timestamp validity end\n  [#1368](https://github.com/sigstore/sigstore-python/pull/1368)\n\n## [3.6.6]\n\n### Changed\n\n* Improved error message when verifying bundles with rekor v2 entries\n  ([#1565](https://github.com/sigstore/sigstore-python/pull/1565))\n* Added cryptography 46 to list of compatible cryptography releases\n  ([#1566](https://github.com/sigstore/sigstore-python/pull/1566))\n\n## [3.6.5]\n\n### Fixed\n\n* Fixed verified time handling so that additional timestamps cannot break\n  otherwise valid signature bundles ([#1492](https://github.com/sigstore/sigstore-python/pull/1492))\n \n### Changed\n\n* Added cryptography 45 to list of compatible cryptography releases\n  ([#1498](https://github.com/sigstore/sigstore-python/pull/1498))\n\n\n## [3.6.4]\n\n### Fixed\n\n* Bumped the `rfc3161-client` dependency to `>=1.0.3` to fix a security\n  vulnerability ([#1451](https://github.com/sigstore/sigstore-python/pull/1451))\n\n## [3.6.3]\n\n### Fixed\n\n* Verify: Avoid hard failure if trusted root contains unsupported keytypes (as verification\n  may succeed without that key).\n  [#1425](https://github.com/sigstore/sigstore-python/pull/1425)\n\n## [3.6.2]\n\n### Fixed\n\n* Fixed issue where a trust root with multiple rekor keys was not considered valid:\n  Now any rekor key listed in the trust root is considered good to verify entries\n  [#1350](https://github.com/sigstore/sigstore-python/pull/1350)\n\n### Changed\n\n* Upgraded python-tuf dependency to 6.0: Connections to TUF repository\n  now use system certificates (instead of certifi) and have automatic\n  retries\n* Updated the embedded TUF root to version 12\n\n## [3.6.1]\n\n### Fixed\n\n* Relaxed the transitive dependency on `cryptography` to allow v43 and v44\n  to be resolved\n  ([#1251](https://github.com/sigstore/sigstore-python/pull/1251))\n\n## [3.6.0]\n\n### Added\n\n* API: The DSSE `Envelope` class now performs automatic validation\n  ([#1211](https://github.com/sigstore/sigstore-python/pull/1211))\n\n* API: Added `signature` property to `Envelope` class for accessing raw\n  signature bytes ([#1211](https://github.com/sigstore/sigstore-python/pull/1211))\n\n* Signed timestamps embedded in bundles are now automatically verified\n  against Timestamp Authorities provided within the Trusted Root ([#1206]\n  (https://github.com/sigstore/sigstore-python/pull/1206))\n\n* Bundles are now generated with signed timestamps when signing if the\n  Trusted Root contains one or more Timestamp Authorities\n  ([#1216](https://github.com/sigstore/sigstore-python/pull/1216))\n\n### Removed\n\n* Support for \"detached\" SCTs has been fully removed, aligning\n  sigstore-python with other sigstore clients\n  ([#1236](https://github.com/sigstore/sigstore-python/pull/1236))\n\n### Fixed\n\n* Fixed a CLI parsing bug introduced in 3.5.1 where a warning about\n  verifying legacy bundles was never shown\n  ([#1198](https://github.com/sigstore/sigstore-python/pull/1198))\n\n* Strengthened the requirement that an inclusion promise is present\n  *if* no other source of signed time is present\n  ([#1247](https://github.com/sigstore/sigstore-python/pull/1247))\n\n## [3.5.3]\n\n### Fixed\n\n* Corrective release for [3.5.2]\n\n## [3.5.2]\n\n### Fixed\n\n* Pinned `cryptography` dependency strictly to prevent future breakage\n\n## [3.5.1]\n\n### Fixed\n\n* Fixed a CLI parsing bug introduced in 3.5.0 when attempting\n  to suppress irrelevant warnings\n  ([#1192](https://github.com/sigstore/sigstore-python/pull/1192))\n\n## [3.5.0]\n\n### Added\n\n* CLI: The `sigstore plumbing update-trust-root` command has been added.\n  Like other plumbing-level commands, this is considered unstable and\n  changes are not subject to our semver policy until explicitly noted\n  ([#1174](https://github.com/sigstore/sigstore-python/pull/1174))\n\n### Fixed\n\n* CLI: Fixed an incorrect warning when verifying detached `.crt`/`.sig`\n  inputs ([#1179](https://github.com/sigstore/sigstore-python/pull/1179))\n\n## [3.4.0]\n\n### Changed\n\n* CLI: When verifying, the `--offline` flag now fully disables all online\n  operations, including routine local TUF repository refreshes\n  ([#1143](https://github.com/sigstore/sigstore-python/pull/1143))\n\n* `sigstore-python`'s minimum supported Python version is now 3.9\n\n### Fixed\n\n* CLI: The `sigstore verify` subcommands now always check for a matching\n  input file, rather than unconditionally falling back to matching on a\n  valid `sha256:...` digest pattern\n  ([#1152](https://github.com/sigstore/sigstore-python/pull/1152))\n\n## [3.3.0]\n\n### Added\n\n* CLI: The `sigstore verify` command now outputs the inner in-toto statement\n  when verifying DSSE envelopes. If verification is successful, the output\n  will be the inner in-toto statement. This allows the user to see the\n  statement's predicate, which `sigstore-python` does not verify and should be\n  verified by the user.\n\n* CLI: The `sigstore attest` subcommand has been added. This command is\n  similar to `cosign attest` in that it signs over an artifact and a\n  predicate using a DSSE envelope. This commands requires the user to pass\n  a path to the file containing the predicate, and the predicate type.\n  Currently only the SLSA Provenance v0.2 and v1.0 types are supported.\n\n* CLI: The `sigstore verify` command now supports verifying digests. This means\n  that the user can now pass a digest like `sha256:aaaa....` instead of the\n  path to an artifact, and `sigstore-python` will verify it as if it was the\n  artifact with that digest.\n\n## [3.2.0]\n\n### 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## [3.1.0]\n\n### Added\n\n* API: `dsse.StatementBuilder` has been added. It can be used to construct an\n  in-toto `Statement` for subsequent enveloping and signing.\n  This API is public but is **not considered stable until the next major\n  release.**\n  ([#1077](https://github.com/sigstore/sigstore-python/pull/1077))\n\n* API: `dsse.Digest`, `dsse.DigestSet`, and `dsse.Subject` have been added.\n  These types can be used with the `StatementBuilder` API as part of in-toto\n  `Statement` construction.\n  These API are public but are **not considered stable until the next major\n  release.**\n  ([#1078](https://github.com/sigstore/sigstore-python/pull/1078))\n\n### Changed\n\n* API: `verify_dsse` now rejects bundles with DSSE envelopes that have more than\n  one signature, rather than checking all signatures against the same key\n  ([#1062](https://github.com/sigstore/sigstore-python/pull/1062))\n\n## [3.0.0]\n\nMaintainers' note: this is a major release, with significant public API and CLI\nchanges. We **strongly** recommend you read the entries below to fully\nunderstand the changes between `2.x` and `3.x`.\n\n### Added\n\n* API: `Signer.sign_artifact()` has been added, replacing the removed\n  `Signer.sign()` API\n\n* API: `Signer.sign_dsse()` has been added. It takes an in-toto `Statement`\n  as an input, producing a DSSE-formatted signature rather than a \"bare\"\n  signature ([#804](https://github.com/sigstore/sigstore-python/pull/804))\n\n* API: \"v3\" Sigstore bundles are now supported during verification\n  ([#901](https://github.com/sigstore/sigstore-python/pull/901))\n\n* API: `Verifier.verify(...)` can now take a `Hashed` as an input, performing\n  signature verification on a pre-computed hash value\n  ([#904](https://github.com/sigstore/sigstore-python/pull/904))\n\n* API: The `sigstore.dsse` module has been been added, including APIs\n  for representing in-toto statements and DSSE envelopes\n  ([#930](https://github.com/sigstore/sigstore-python/pull/930))\n\n* CLI: The `--trust-config` flag has been added as a global option,\n  enabling consistent \"BYO PKI\" uses of `sigstore` with a single flag\n  ([#1010](https://github.com/sigstore/sigstore-python/pull/1010))\n\n* CLI: The `sigstore verify` subcommands can now verify bundles containing\n  DSSE entries, such as those produced by\n  [GitHub Artifact Attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)\n  ([#1015](https://github.com/sigstore/sigstore-python/pull/1015))\n\n### Removed\n\n* **BREAKING API CHANGE**: `SigningResult` has been removed.\n  The public signing APIs now return `sigstore.models.Bundle`.\n\n* **BREAKING API CHANGE**: `VerificationMaterials` has been removed.\n  The public verification APIs now accept `sigstore.models.Bundle`.\n\n* **BREAKING API CHANGE**: `Signer.sign(...)` has been removed. Use\n  either `sign_artifact(...)` or `sign_dsse(...)`, depending on whether\n  you're signing opaque bytes or an in-toto statement.\n\n* **BREAKING API CHANGE**: `VerificationResult` has been removed.\n  The public verification and policy APIs now raise\n  `sigstore.errors.VerificationError` on failure.\n\n* **BREAKING CLI CHANGE**: The `--rekor-url` and `--fulcio-url`\n  flags have been entirely removed. To configure a custom PKI, use\n  `--trust-config`\n  ([#1010](https://github.com/sigstore/sigstore-python/pull/1010))\n\n### Changed\n\n* **BREAKING API CHANGE**: `Verifier.verify(...)`  now takes a `bytes | Hashed`\n  as its verification input, rather than implicitly receiving the input through\n  the `VerificationMaterials` parameter\n  ([#904](https://github.com/sigstore/sigstore-python/pull/904))\n\n* **BREAKING API CHANGE**: `VerificationMaterials.rekor_entry(...)` now takes\n  a `Hashed` parameter to convey the digest used for Rekor entry lookup\n  ([#904](https://github.com/sigstore/sigstore-python/pull/904))\n\n* **BREAKING API CHANGE**: `Verifier.verify(...)` now takes a `sigstore.models.Bundle`,\n  instead of a `VerificationMaterials` ([#937](https://github.com/sigstore/sigstore-python/pull/937))\n\n* **BREAKING CLI CHANGE**: `sigstore sign` now emits `{input}.sigstore.json`\n  by default instead of `{input}.sigstore`, per the client specification\n  ([#1007](https://github.com/sigstore/sigstore-python/pull/1007))\n\n* sigstore-python now requires inclusion proofs in all signing and verification\n  flows, regardless of bundle version of input types. Inputs that do not\n  have an inclusion proof (such as detached materials) cause an online lookup\n  before any further processing is performed\n  ([#937](https://github.com/sigstore/sigstore-python/pull/937))\n\n* sigstore-python now generates \"v3\" bundles by default during signing\n  ([#937](https://github.com/sigstore/sigstore-python/pull/937))\n\n* CLI: Bundles are now always verified offline. The offline flag has no effect.\n  ([#937](https://github.com/sigstore/sigstore-python/pull/937))\n\n* CLI: \"Detached\" materials are now always verified online, due to a lack of\n  an inclusion proof. Passing `--offline` with detached materials will cause\n  an error ([#937](https://github.com/sigstore/sigstore-python/pull/937))\n\n* API: `sigstore.transparency` has been removed, and its pre-existing APIs\n  have been re-homed under `sigstore.models`\n  ([#990](https://github.com/sigstore/sigstore-python/pull/990))\n\n* API: `oidc.IdentityToken.expected_certificate_subject` has been renamed\n  to `oidc.IdentityToken.federated_issuer` to better describe what it actually\n  contains. No functional changes have been made to it\n  ([#1016](https://github.com/sigstore/sigstore-python/pull/1016))\n\n* API: `policy.Identity` now takes an **optional** OIDC issuer, rather than a\n  required one ([#1015](https://github.com/sigstore/sigstore-python/pull/1015))\n\n* CLI: `sigstore verify github` now requires `--cert-identity` **or**\n  `--repository`, not just `--cert-identity`\n  ([#1015](https://github.com/sigstore/sigstore-python/pull/1015))\n\n## [2.1.5]\n\n## Fixed\n\n* Backported b32ad1bd (slsa-github-generator upgrade) to make release possible\n\n## [2.1.4]\n\n## Fixed\n\n* Pinned `securesystemslib` dependency strictly to prevent future breakage\n\n## [2.1.3]\n\n## Fixed\n\n* Loosened a version constraint on the `sigstore-protobuf-specs` dependency,\n  to ease use in testing environments\n  ([#943](https://github.com/sigstore/sigstore-python/pull/943))\n\n## [2.1.2]\n\nThis is a corrective release for [2.1.1].\n\n## [2.1.1]\n\n### Fixed\n\n* Fixed an incorrect assumption about Rekor checkpoints that future releases\n  of Rekor will not uphold ([#891](https://github.com/sigstore/sigstore-python/pull/891))\n\n## [2.1.0]\n\n### Added\n\n* CLI: `sigstore verify`'s subcommands now discover `{input}.sigstore.json`\n  by default, in addition to the previous `{input}.sigstore`. The former now\n  takes precedence over the latter, and supplying both results in an error\n  ([#820](https://github.com/sigstore/sigstore-python/pull/820))\n\n## [2.0.1]\n\n### Fixed\n\n* CLI: When using `--certificate-chain`, read as `bytes` instead of `str`\n  as expected by the underlying API ([#796](https://github.com/sigstore/sigstore-python/pull/796))\n\n## [2.0.0]\n\n### Added\n\n* CLI: `sigstore sign` and `sigstore get-identity-token` now support the\n  `--oauth-force-oob` option; which has the same behavior as the\n  preexisting `SIGSTORE_OAUTH_FORCE_OOB` environment variable\n  ([#667](https://github.com/sigstore/sigstore-python/pull/667))\n\n* Version `0.2` of the Sigstore bundle format is now supported\n  ([#705](https://github.com/sigstore/sigstore-python/pull/705))\n\n* API addition: `VerificationMaterials.to_bundle()` is a new public API for\n  producing a standard Sigstore bundle from `sigstore-python`'s internal\n  representation ([#719](https://github.com/sigstore/sigstore-python/pull/719))\n\n* API addition: New method `sign.SigningResult.to_bundle()` allows signing\n  applications to serialize to the bundle format that is already usable in\n  verification with `verify.VerificationMaterials.from_bundle()`\n  ([#765](https://github.com/sigstore/sigstore-python/pull/765))\n\n### Changed\n\n* `sigstore verify` now performs additional verification of Rekor's inclusion\n  proofs by cross-checking them against signed checkpoints\n  ([#634](https://github.com/sigstore/sigstore-python/pull/634))\n\n* A cached copy of the trust bundle is now included with the distribution\n  ([#611](https://github.com/sigstore/sigstore-python/pull/611))\n\n* Stopped emitting .sig and .crt signing outputs by default in `sigstore sign`.\n  Sigstore bundles are now preferred\n  ([#614](https://github.com/sigstore/sigstore-python/pull/614))\n\n* Trust root configuration now assumes that the TUF repository contains a trust\n  bundle, rather than falling back to deprecated individual targets\n  ([#626](https://github.com/sigstore/sigstore-python/pull/626))\n\n* API change: the `sigstore.oidc.IdentityToken` API has been stabilized as\n  a wrapper for OIDC tokens\n  ([#635](https://github.com/sigstore/sigstore-python/pull/635))\n\n* API change: `Signer.sign` now takes a `sigstore.oidc.IdentityToken` for\n  its `identity` argument, rather than a \"raw\" OIDC token\n  ([#635](https://github.com/sigstore/sigstore-python/pull/635))\n\n* API change: `Issuer.identity_token` now returns a\n  `sigstore.oidc.IdentityToken`, rather than a \"raw\" OIDC token\n  ([#635](https://github.com/sigstore/sigstore-python/pull/635))\n\n* `sigstore verify` is not longer a backwards-compatible alias for\n  `sigstore verify identity`, as it was during the 1.0 release series\n  ([#642](https://github.com/sigstore/sigstore-python/pull/642))\n\n* API change: the `Signer` API has been broken up into `SigningContext`\n  and `Signer`, allowing a `SigningContext` to create individual `Signer`\n  instances that correspond to a single `IdentityToken`. This new API\n  also enables ephemeral key and certificate reuse across multiple inputs,\n  reducing the number of cryptographic operations and network roundtrips\n  required when signing more than one input\n  ([#645](https://github.com/sigstore/sigstore-python/pull/645))\n\n* `sigstore sign` now uses an ephemeral P-256 keypair, rather than P-384\n  ([#662](https://github.com/sigstore/sigstore-python/pull/662))\n\n* API change: `RekorClientError` does not try to always parse response\n  content as JSON\n  ([#694](https://github.com/sigstore/sigstore-python/pull/694))\n\n* API change: `LogEntry.inclusion_promise` can now be `None`, but only\n  if `LogEntry.inclusion_proof` is not `None`\n  ([#705](https://github.com/sigstore/sigstore-python/pull/705))\n\n* `sigstore-python`'s minimum supported Python version is now 3.8\n  ([#745](https://github.com/sigstore/sigstore-python/pull/745))\n\n### Fixed\n\n* Fixed a case where `sigstore verify` would fail to verify an otherwise valid\n  inclusion proof due to an incorrect timerange check\n  ([#633](https://github.com/sigstore/sigstore-python/pull/633))\n\n* Removed an unnecessary and backwards-incompatible parameter from the\n  `sigstore.oidc.detect_credential` API\n  ([#641](https://github.com/sigstore/sigstore-python/pull/641))\n\n* Fixed a case where `sigstore sign` (and `sigstore verify`) could fail while\n  using a private instance due to a missing due to a missing `ExtendedKeyUsage`\n  in the CA. We now enforce the fact that the TBSPrecertificate signer must be\n  a valid CA ([#658](https://github.com/sigstore/sigstore-python/pull/658))\n\n* Fixed a case where identity token retrieval would produce an unhelpful\n  error message ([#767](https://github.com/sigstore/sigstore-python/pull/767))\n\n## [1.1.2]\n\n### Fixed\n\n* Updated the `staging-root.json` for recent changes to the Sigstore staging\n  instance ([#602](https://github.com/sigstore/sigstore-python/pull/602))\n\n* Switched TUF requests to their CDN endpoints, rather than direct GCS\n  access ([#609](https://github.com/sigstore/sigstore-python/pull/609))\n\n## [1.1.1]\n\n### Added\n\n* `sigstore sign` now supports the `--output-directory` flag, which places\n  default outputs in the specified directory. Without this flag, default outputs\n  are placed adjacent to the signing input.\n  ([#627](https://github.com/sigstore/sigstore-python/pull/627))\n\n* The whole test suite can now be run locally with `make test-interactive`.\n  ([#576](https://github.com/sigstore/sigstore-python/pull/576))\n  Users will be prompted to authenticate with their identity provider twice to\n  generate staging and production OIDC tokens, which are used to test the\n  `sigstore.sign` module. All signing tests need to be completed before token\n  expiry, which is currently 60 seconds after issuance.\n\n* Network-related errors from the `sigstore._internal.tuf` module now have better\n  diagnostics.\n  ([#525](https://github.com/sigstore/sigstore-python/pull/525))\n\n### Changed\n\n* Replaced ambient credential detection logic with the `id` package\n  ([#535](https://github.com/sigstore/sigstore-python/pull/535))\n\n* Revamped error diagnostics reporting. All errors with diagnostics now implement\n  `sigstore.errors.Error`.\n\n* Trust root materials are now retrieved from a single trust bundle,\n  if it is available via TUF\n  ([#542](https://github.com/sigstore/sigstore-python/pull/542))\n\n* Improved diagnostics around Signed Certificate Timestamp verification failures.\n  ([#555](https://github.com/sigstore/sigstore-python/pull/555))\n\n### Fixed\n\n* Fixed a bug in TUF target handling revealed by changes to the production\n  and staging TUF repos\n  ([#522](https://github.com/sigstore/sigstore-python/pull/522))\n\n## [1.1.0]\n\n### Added\n\n* `sigstore sign` now supports Sigstore bundles, which encapsulate the same\n  state as the default `{input}.crt`, `{input}.sig`, and `{input}.rekor`\n  files combined. The default output for the Sigstore bundle is\n  `{input}.sigstore`; this can be disabled with `--no-bundle` or changed with\n  `--bundle <FILE>`\n  ([#465](https://github.com/sigstore/sigstore-python/pull/465))\n\n* `sigstore verify` now supports Sigstore bundles. By default, `sigstore` looks\n  for an `{input}.sigstore`; this can be changed with `--bundle <FILE>` or the\n  legacy method of verification can be used instead via the `--signature` and\n  `--certificate` flags\n  ([#478](https://github.com/sigstore/sigstore-python/pull/478))\n\n* `sigstore verify identity` and `sigstore verify github` now support the\n  `--offline` flag, which tells `sigstore` to do offline transparency log\n  entry verification. This option replaces the unstable\n  `--require-rekor-offline` option, which has been removed\n  ([#478](https://github.com/sigstore/sigstore-python/pull/478))\n\n### Fixed\n\n* Constrained our dependency on `pyOpenSSL` to `>= 23.0.0` to prevent\n  a runtime error caused by incompatible earlier versions\n  ([#448](https://github.com/sigstore/sigstore-python/pull/448))\n\n### Removed\n\n* `--rekor-bundle` and `--require-rekor-offline` have been removed entirely,\n  as their functionality have been wholly supplanted by Sigstore bundle support\n  and the new `sigstore verify --offline` flag\n  ([#478](https://github.com/sigstore/sigstore-python/pull/478))\n\n## [1.0.0]\n\n### Changed\n\n* `sigstore.rekor` is now `sigstore.transparency`, and its constituent APIs\n  have been renamed to removed implementation detail references\n  ([#402](https://github.com/sigstore/sigstore-python/pull/402))\n\n* `sigstore.transparency.RekorEntryMissing` is now `LogEntryMissing`\n  ([#414](https://github.com/sigstore/sigstore-python/pull/414))\n\n### Fixed\n\n* The TUF network timeout has been relaxed from 4 seconds to 30 seconds,\n  which should reduce the likelihood of spurious timeout errors in environments\n  like GitHub Actions ([#432](https://github.com/sigstore/sigstore-python/pull/432))\n\n## [0.10.0]\n\n### Added\n\n* `sigstore` now supports the `-v`/`--verbose` flag as an alternative to\n  `SIGSTORE_LOGLEVEL` for debug logging\n  ([#372](https://github.com/sigstore/sigstore-python/pull/372))\n\n* The `sigstore verify identity` has been added, and is functionally\n  equivalent to the existing `sigstore verify` subcommand.\n  `sigstore verify` is unchanged, but will be marked deprecated in a future\n  stable version of `sigstore-python`\n  ([#379](https://github.com/sigstore/sigstore-python/pull/379))\n\n* `sigstore` now has a public, importable Python API! You can find its\n  documentation [here](https://sigstore.github.io/sigstore-python/)\n  ([#383](https://github.com/sigstore/sigstore-python/pull/383))\n\n* `sigstore --staging` is now the intended way to request Sigstore's staging\n   instance, rather than per-subcommand options like `sigstore sign --staging`.\n   The latter is unchanged, but will be marked deprecated in a future stable\n   version of `sigstore-python`\n   ([#383](https://github.com/sigstore/sigstore-python/pull/383))\n\n* The per-subcommand options `--rekor-url` and `--rekor-root-pubkey` have been\n  moved to the top-level `sigstore` command. Their subcommand forms are unchanged\n  and will continue to work, but will be marked deprecated in a future stable\n  version of `sigstore-python`\n  ([#381](https://github.com/sigstore/sigstore-python/pull/383))\n\n* `sigstore verify github` has been added, allowing for verification of\n  GitHub-specific claims within given certificate(s)\n  ([#381](https://github.com/sigstore/sigstore-python/pull/381))\n\n### Changed\n\n* The default behavior of `SIGSTORE_LOGLEVEL` has changed; the logger\n  configured is now the `sigstore.*` hierarchy logger, rather than the \"root\"\n  logger ([#372](https://github.com/sigstore/sigstore-python/pull/372))\n\n* The caching mechanism used for TUF has been changed slightly, to use\n  more future-proof paths ([#373](https://github.com/sigstore/sigstore-python/pull/373))\n\n### Fixed\n\n* Fulcio certificate handling now includes \"inactive\" but still valid certificates,\n  allowing users to verify older signatures without custom certificate chains\n  ([#386](https://github.com/sigstore/sigstore-python/pull/386))\n\n## [0.9.0]\n\n### Added\n\n* `sigstore verify` now supports `--certificate-chain` and `--rekor-url`\n  during verification. Ordinary uses (i.e. the default or `--staging`)\n  are not affected ([#323](https://github.com/sigstore/sigstore-python/pull/323))\n\n### Changed\n\n* `sigstore sign` and `sigstore verify` now stream their input, rather than\n  consuming it into a single buffer\n  ([#329](https://github.com/sigstore/sigstore-python/pull/329))\n\n* A series of Python 3.11 deprecation warnings were eliminated\n  ([#341](https://github.com/sigstore/sigstore-python/pull/341))\n\n* The \"splash\" page presented to users during the OAuth flow has been updated\n  to reflect the user-friendly page added to `cosign`\n  ([#356](https://github.com/sigstore/sigstore-python/pull/356))\n\n* `sigstore` now uses TUF to retrieve its trust material for Fulcio and Rekor,\n  replacing the material that was previously baked into `sigstore._store`\n  ([#351](https://github.com/sigstore/sigstore-python/pull/351))\n\n### Removed\n* CLI: The `--certificate-chain`, `--rekor-root-pubkey` and `-ctfe` flags have been entirely removed ([#936](https://github.com/sigstore/sigstore-python/pull/936))\n\n\n<!--Release URLs -->\n[Unreleased]: https://github.com/sigstore/sigstore-python/compare/v4.2.0...HEAD\n[4.2.0]: https://github.com/sigstore/sigstore-python/compare/v4.1.0...v4.2.0\n[4.1.0]: https://github.com/sigstore/sigstore-python/compare/v4.0.0...v4.1.0\n[4.0.0]: https://github.com/sigstore/sigstore-python/compare/v3.6.5...v4.0.0\n[3.6.5]: https://github.com/sigstore/sigstore-python/compare/v3.6.4...v3.6.5\n[3.6.4]: https://github.com/sigstore/sigstore-python/compare/v3.6.3...v3.6.4\n[3.6.3]: https://github.com/sigstore/sigstore-python/compare/v3.6.2...v3.6.3\n[3.6.2]: https://github.com/sigstore/sigstore-python/compare/v3.6.1...v3.6.2\n[3.6.1]: https://github.com/sigstore/sigstore-python/compare/v3.6.0...v3.6.1\n[3.6.0]: https://github.com/sigstore/sigstore-python/compare/v3.5.3...v3.6.0\n[3.5.3]: https://github.com/sigstore/sigstore-python/compare/v3.5.2...v3.5.3\n[3.5.2]: https://github.com/sigstore/sigstore-python/compare/v3.5.1...v3.5.2\n[3.5.1]: https://github.com/sigstore/sigstore-python/compare/v3.5.0...v3.5.1\n[3.5.0]: https://github.com/sigstore/sigstore-python/compare/v3.4.0...v3.5.0\n[3.4.0]: https://github.com/sigstore/sigstore-python/compare/v3.3.0...v3.4.0\n[3.3.0]: https://github.com/sigstore/sigstore-python/compare/v3.2.0...v3.3.0\n[3.2.0]: https://github.com/sigstore/sigstore-python/compare/v3.1.0...v3.2.0\n[3.1.0]: https://github.com/sigstore/sigstore-python/compare/v3.0.0...v3.1.0\n[3.0.0]: https://github.com/sigstore/sigstore-python/compare/v2.1.5...v3.0.0\n[2.1.5]: https://github.com/sigstore/sigstore-python/compare/v2.1.4...v2.1.5\n[2.1.4]: https://github.com/sigstore/sigstore-python/compare/v2.1.3...v2.1.4\n[2.1.3]: https://github.com/sigstore/sigstore-python/compare/v2.1.2...v2.1.3\n[2.1.2]: https://github.com/sigstore/sigstore-python/compare/v2.1.1...v2.1.2\n[2.1.1]: https://github.com/sigstore/sigstore-python/compare/v2.1.0...v2.1.1\n[2.1.0]: https://github.com/sigstore/sigstore-python/compare/v2.0.1...v2.1.0\n[2.0.1]: https://github.com/sigstore/sigstore-python/compare/v2.0.0...v2.0.1\n[2.0.0]: https://github.com/sigstore/sigstore-python/compare/v1.1.2...v2.0.0\n[1.1.2]: https://github.com/sigstore/sigstore-python/compare/v1.1.1...v1.1.2\n[1.1.1]: https://github.com/sigstore/sigstore-python/compare/v1.1.0...v1.1.1\n[1.1.0]: https://github.com/sigstore/sigstore-python/compare/v1.0.0...v1.1.0\n[1.0.0]: https://github.com/sigstore/sigstore-python/compare/v0.10.0...v1.0.0\n[0.10.0]: https://github.com/sigstore/sigstore-python/compare/v0.9.0...v0.10.0\n[0.9.0]: https://github.com/sigstore/sigstore-python/compare/v0.8.3...v0.9.0\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "@sigstore/codeowners-sigstore-python\n\n# The CODEOWNERS are managed via a GitHub team, but the current list is (in alphabetical order):\n\n# di\n# tetsuo-cpp\n# woodruffw\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Contributing to sigstore\n========================\n\nThank you for your interest in contributing to `sigstore`!\n\nThe information below will help you set up a local development environment,\nas well as performing common development tasks.\n\n## Requirements\n\n`sigstore`'s only development environment requirement *should* be Python 3.10\nor newer. Development and testing is actively performed on macOS and Linux,\nbut Windows and other supported platforms that are supported by Python\nshould also work.\n\nIf you're on a system that has GNU Make, you can use the convenience targets\nincluded in the `Makefile` that comes in the `sigstore` repository detailed\nbelow. But this isn't required; all steps can be done without Make.\n\n## Development steps\n\nFirst, clone this repository:\n\n```bash\ngit clone https://github.com/sigstore/sigstore-python\ncd sigstore\n```\n\nThen, use one of the `Makefile` targets to run a task. The first time this is\nrun, this will also set up the local development virtual environment, and will\ninstall `sigstore` as an editable package into this environment.\n\nAny changes you make to the `sigstore` source tree will take effect\nimmediately in the virtual environment.\n\n### Linting\n\nYou can lint locally with:\n\n```bash\nmake lint\n```\n\n`sigstore` is automatically linted and formatted with a collection of tools:\n\n* [`ruff`](https://github.com/charliermarsh/ruff): Code formatting, PEP-8 linting, style enforcement\n* [`mypy`](https://mypy.readthedocs.io/en/stable/): Static type checking\n* [`bandit`](https://github.com/PyCQA/bandit): Security issue scanning\n* [`interrogate`](https://interrogate.readthedocs.io/en/latest/): Documentation coverage\n\n\nTo automatically apply any lint-suggested changes, you can run:\n\n```bash\nmake reformat\n```\n\n### Testing\n\nYou can run the tests locally with:\n\n```bash\nmake test\n```\n\nor:\n\n```bash\nmake test-interactive\n```\n\nto run tests that require OIDC credentials (will prompt for authentication to generate tokens).\nNote that `test-interactive` may fail if you have a slow network, as the tokens generated are only\nvalid for 60 seconds after their issuance.\n\nYou can also filter by a pattern (uses `pytest -k`):\n\n```bash\nmake test TESTS=test_version\n```\n\nTo test a specific file:\n\n```bash\nmake test T=path/to/file.py\n```\n\n`sigstore` has a [`pytest`](https://docs.pytest.org/)-based unit test suite,\nincluding code coverage with [`coverage.py`](https://coverage.readthedocs.io/).\n\n#### X.509 test cases\n\n`sigstore` includes some checked-in X.509 test assets under\n[`test/unit/assets/x509`](./test/unit/assets/x509/).\n\nThese assets are generated by the adjacent\n[`build-testcases.py`](./test/unit/assets/x509/build-testcases.py) script,\nwhich can be updated to generate additional test cases.\n\nTo re-build the X.509 test cases, you can use `make`:\n\n```bash\nmake gen-x509-testcases\n```\n\n### Documentation\n\nIf you're running Python 3.10 or newer, you can run the documentation build locally:\n\n```bash\nmake doc\n```\n\n`sigstore` uses [`pdoc`](https://github.com/mitmproxy/pdoc) to generate HTML\ndocumentation for the public Python APIs.\n\n### Releasing\n\n**NOTE**: If you're a non-maintaining contributor, you don't need the steps\nhere! They're documented for completeness and for onboarding future maintainers.\n\nReleases of `sigstore` are managed with [`bump`](https://github.com/di/bump)\nand GitHub Actions.\n\n```bash\n# default release (patch bump)\nmake release\n\n# override the default\n# vX.Y.Z -> vX.Y.Z-rc.0\nmake release BUMP_ARGS=\"--pre rc.0\"\n\n# vX.Y.Z -> vN.0.0\nmake release BUMP_ARGS=\"--major\"\n```\n\n`make release` will fail if there are any untracked changes in the source tree.\n\nIf `make release` succeeds, you'll see an output like this:\n\n```\nRUN ME MANUALLY: git push origin main && git push origin vX.Y.Z\n```\n\nRun that last command sequence to complete the release.\n\n## Development practices\n\nHere are some guidelines to follow if you're working on a new feature or changes to\n`sigstore`'s internal APIs:\n\n* *Keep the `sigstore` APIs as private as possible*. Nearly all of `sigstore`'s\nAPIs should be private and treated as unstable and unsuitable for public use.\nIf you're adding a new module to the source tree, prefix the filename with an underscore to\nemphasize that it's an internal (e.g., `sigstore/_foo.py` instead of `sigstore/foo.py`).\n\n* *Perform judicious debug logging.* `sigstore` uses the standard Python\n[`logging`](https://docs.python.org/3/library/logging.html) module. Use\n`logger.debug` early and often -- users who experience errors can submit better\nbug reports when their debug logs include helpful context!\n\n* *Update the [CHANGELOG](./CHANGELOG.md)*. If your changes are public or result\nin changes to `sigstore`'s CLI, please record them under the \"Unreleased\" section,\nwith an entry in an appropriate subsection (\"Added\", \"Changed\", \"Removed\", or \"Fixed\").\n\n* Ensure your commits are signed off, as sigstore uses the\n[DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin).\nYou can do it using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits.\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL := /bin/bash\n\nPY_MODULE := sigstore\n\nALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py') \\\n\t$(shell find test -name '*.py') \\\n\t$(shell find docs/scripts -name '*.py') \\\n\n# Optionally overridden by the user, if they're using a virtual environment manager.\nVENV ?= env\n\n# On Windows, venv scripts/shims are under `Scripts` instead of `bin`.\nVENV_BIN := $(VENV)/bin\nifeq ($(OS),Windows_NT)\n\tVENV_BIN := $(VENV)/Scripts\nendif\n\n# Optionally overridden by the user in the `release` target.\nBUMP_ARGS :=\n\n# Optionally overridden by the user in the `test` target.\nTESTS ?=\n\n# Optionally overridden by the user/CI, to limit the installation to a specific\n# subset of development dependencies.\nSIGSTORE_EXTRA := dev\n\n# If the user selects a specific test pattern to run, set `pytest` to fail fast\n# and only run tests that match the pattern.\n# Otherwise, run all tests and enable coverage assertions, since we expect\n# complete test coverage.\nifneq ($(TESTS),)\n\tTEST_ARGS := -x -k $(TESTS) $(TEST_ARGS)\n\tCOV_ARGS :=\nelse\n\tTEST_ARGS := $(TEST_ARGS)\n# TODO: Re-enable coverage testing\n#\tCOV_ARGS := --fail-under 100\nendif\n\nifneq ($(T),)\n\tT := $(T)\nelse\n\tT := test/unit test/integration\nendif\n\n.PHONY: all\nall:\n\t@echo \"Run my targets individually!\"\n\n$(VENV)/pyvenv.cfg: pyproject.toml\n\t# Create our Python 3 virtual environment\n\tpython3 -m venv $(VENV)\n\t$(VENV_BIN)/python -m pip install --upgrade pip\n\t$(VENV_BIN)/python -m pip install -e .[$(SIGSTORE_EXTRA)]\n\n.PHONY: dev\ndev: $(VENV)/pyvenv.cfg\n\n.PHONY: run\nrun: $(VENV)/pyvenv.cfg\n\t@. $(VENV_BIN)/activate && sigstore $(ARGS)\n\n.PHONY: lint\nlint: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\truff format --check $(ALL_PY_SRCS) && \\\n\t\truff check $(ALL_PY_SRCS) && \\\n\t\tmypy $(PY_MODULE) && \\\n\t\tbandit -c pyproject.toml -r $(PY_MODULE) && \\\n\t\tpython docs/scripts/gen_ref_pages.py --check\n\n.PHONY: reformat\nreformat: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\truff check --fix $(ALL_PY_SRCS) && \\\n\t\truff format $(ALL_PY_SRCS)\n\n.PHONY: test\ntest: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\t$(TEST_ENV) pytest --cov-append --cov=$(PY_MODULE) $(T) $(TEST_ARGS) && \\\n\t\tpython -m coverage report -m $(COV_ARGS)\n\n.PHONY: test-interactive\ntest-interactive: TEST_ENV += \\\n\tSIGSTORE_IDENTITY_TOKEN_production=$$($(MAKE) -s run ARGS=\"get-identity-token\") \\\n\tSIGSTORE_IDENTITY_TOKEN_staging=$$($(MAKE) -s run ARGS=\"--staging get-identity-token\")\ntest-interactive: test\n\n.PHONY: gen-x509-testcases\ngen-x509-testcases: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\texport TESTCASE_OVERWRITE=1 && \\\n\t\tpython test/assets/x509/build-testcases.py && \\\n\t\tgit diff --exit-code\n\n.PHONY: doc\ndoc: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\tpython docs/scripts/gen_ref_pages.py --overwrite && \\\n\t\tmkdocs build --strict --site-dir html\n\n.PHONY: package\npackage: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\tpython3 -m build\n\n.PHONY: release\nrelease: $(VENV)/pyvenv.cfg\n\t@. $(VENV_BIN)/activate && \\\n\t\tNEXT_VERSION=$$(bump $(BUMP_ARGS)) && \\\n\t\tgit add $(PY_MODULE)/_version.py && git diff --quiet --exit-code && \\\n\t\tgit commit -m \"version: v$${NEXT_VERSION}\" && \\\n\t\tgit tag v$${NEXT_VERSION} && \\\n\t\techo \"RUN ME MANUALLY: git push origin main && git push origin v$${NEXT_VERSION}\"\n\n.PHONY: check-readme\ncheck-readme:\n\t# sigstore --help\n\t@diff \\\n\t  <( \\\n\t    awk '/@begin-sigstore-help@/{f=1;next} /@end-sigstore-help@/{f=0} f' \\\n\t      < README.md | sed '1d;$$d' \\\n\t  ) \\\n\t  <( \\\n\t    $(MAKE) -s run ARGS=\"--help\" \\\n\t  )\n\n\t# sigstore sign --help\n\t@diff \\\n\t  <( \\\n\t    awk '/@begin-sigstore-sign-help@/{f=1;next} /@end-sigstore-sign-help@/{f=0} f' \\\n\t      < README.md | sed '1d;$$d' \\\n\t  ) \\\n\t  <( \\\n\t    $(MAKE) -s run ARGS=\"sign --help\" \\\n\t  )\n\n\t# sigstore attest --help\n\t@diff \\\n\t  <( \\\n\t    awk '/@begin-sigstore-attest-help@/{f=1;next} /@end-sigstore-attest-help@/{f=0} f' \\\n\t      < README.md | sed '1d;$$d' \\\n\t  ) \\\n\t  <( \\\n\t    $(MAKE) -s run ARGS=\"attest --help\" \\\n\t  )\n\n\t# sigstore verify identity --help\n\t@diff \\\n\t  <( \\\n\t    awk '/@begin-sigstore-verify-identity-help@/{f=1;next} /@end-sigstore-verify-identity-help@/{f=0} f' \\\n\t      < README.md | sed '1d;$$d' \\\n\t  ) \\\n\t  <( \\\n\t    $(MAKE) -s run ARGS=\"verify identity --help\" \\\n\t  )\n\n\t# sigstore verify github --help\n\t@diff \\\n\t  <( \\\n\t    awk '/@begin-sigstore-verify-github-help@/{f=1;next} /@end-sigstore-verify-github-help@/{f=0} f' \\\n\t      < README.md | sed '1d;$$d' \\\n\t  ) \\\n\t  <( \\\n\t    $(MAKE) -s run ARGS=\"verify github --help\" \\\n\t  )\n\n\n.PHONY: edit\nedit:\n\t$(EDITOR) $(ALL_PY_SRCS)\n\nupdate-embedded-root: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\tpython -m sigstore plumbing update-trust-root\n\tcp ~/.local/share/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json \\\n\t\tsigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json\n\tcp ~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json \\\n\t\t~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/signing_config.v0.2.json \\\n\t\tsigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/\n\nupdate-embedded-root-staging: $(VENV)/pyvenv.cfg\n\t. $(VENV_BIN)/activate && \\\n\t\tpython -m sigstore --staging plumbing update-trust-root\n\tcp ~/.local/share/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/root.json \\\n\t\tsigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/root.json\n\tcp ~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/trusted_root.json \\\n\t\t~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/signing_config.v0.2.json \\\n\t\tsigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/\n"
  },
  {
    "path": "README.md",
    "content": "sigstore-python\n===============\n\n<!--- @begin-badges@ --->\n[![CI](https://github.com/sigstore/sigstore-python/workflows/CI/badge.svg)](https://github.com/sigstore/sigstore-python/actions/workflows/ci.yml)\n[![PyPI version](https://badge.fury.io/py/sigstore.svg)](https://pypi.org/project/sigstore)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/sigstore/sigstore-python/badge)](https://securityscorecards.dev/viewer/?uri=github.com/sigstore/sigstore-python)\n[![SLSA](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev/)\n![Conformance Tests](https://github.com/sigstore/sigstore-python/workflows/Conformance%20Tests/badge.svg)\n[![Documentation](https://github.com/sigstore/sigstore-python/actions/workflows/docs.yml/badge.svg)](https://sigstore.github.io/sigstore-python)\n<!--- @end-badges@ --->\n\n`sigstore` is a Python tool for generating and verifying Sigstore signatures.\nYou can use it to sign and verify Python package distributions, or anything\nelse!\n\n## Index\n\n* [Features](#features)\n* [Installation](#installation)\n* [Usage](#usage)\n  * [Signing](#signing)\n  * [Verifying](#verifying)\n    * [Generic identities](#generic-identities)\n    * [Signatures from GitHub Actions](#signatures-from-github-actions)\n  * [Advanced usage](#advanced-usage)\n* [Troubleshooting](#troubleshooting)\n* [Documentation](#documentation)\n* [Licensing](#licensing)\n* [Community](#community)\n* [Contributing](#contributing)\n* [Code of Conduct](#code-of-conduct)\n* [Security](#security)\n* [SLSA Provenance](#slsa-provenance)\n\n## Features\n\n* Support for keyless signature generation and verification with [Sigstore](https://www.sigstore.dev/)\n* Support for signing with [\"ambient\" OpenID Connect identities](https://github.com/sigstore/sigstore-python#signing-with-ambient-credentials)\n* A comprehensive [CLI](https://github.com/sigstore/sigstore-python#usage) and corresponding\n  [importable Python API](https://sigstore.github.io/sigstore-python)\n\n## Installation\n\n`sigstore` requires Python 3.10 or newer, and can be installed directly via `pip`:\n\n```console\npython -m pip install sigstore\n```\n\nSee the [installation](https://sigstore.github.io/sigstore-python/installation) page in the documentation for more\ninstallation options.\n\n## Usage\n\nFor Python API usage, see our [API](https://sigstore.github.io/sigstore-python/api/).\n\nYou can run `sigstore` as a standalone program:\n\n```console\nsigstore --help\n```\n\nTop-level:\n\n<!-- @begin-sigstore-help@ -->\n```\nusage: sigstore [-h] [-v] [-V]\n                [--staging | --instance URL | --trust-config FILE]\n                COMMAND ...\n\na tool for signing and verifying Python package distributions\n\npositional arguments:\n  COMMAND              the operation to perform\n    attest             sign one or more inputs using DSSE\n    sign               sign one or more inputs\n    verify             verify one or more inputs\n    get-identity-token\n                       retrieve and return a Sigstore-compatible OpenID\n                       Connect token\n    trust-instance     Initialize trust for a Sigstore instance\n    plumbing           developer-only plumbing operations\n\noptions:\n  -h, --help           show this help message and exit\n  -v, --verbose        run with additional debug logging; supply multiple\n                       times to increase verbosity (default: 0)\n  -V, --version        show program's version number and exit\n  --staging            Use sigstore's staging instance, instead of the default\n                       production instance. Mutually exclusive with other\n                       instance configuration arguments. (default: False)\n  --instance URL       Use a given Sigstore instance URL, instead of the\n                       default production instance. Mutually exclusive with\n                       other instance configuration arguments. (default: None)\n  --trust-config FILE  Use given client trust configuration instead of using\n                       the default production instance. Mutually exclusive\n                       with other instance configuration arguments. (default:\n                       None)\n```\n<!-- @end-sigstore-help@ -->\n\n\n### Signing\n\n<!-- @begin-sigstore-sign-help@ -->\n```\nusage: sigstore sign [-h] [-v] [--rekor-version VERSION]\n                     [--identity-token TOKEN] [--oidc-client-id ID]\n                     [--oidc-client-secret SECRET]\n                     [--oidc-disable-ambient-providers] [--oidc-issuer URL]\n                     [--oauth-force-oob] [--no-default-files]\n                     [--signature FILE] [--certificate FILE] [--bundle FILE]\n                     [--output-directory DIR] [--overwrite]\n                     FILE [FILE ...]\n\npositional arguments:\n  FILE                  The file to sign\n\noptions:\n  -h, --help            show this help message and exit\n  -v, --verbose         run with additional debug logging; supply multiple\n                        times to increase verbosity (default: 0)\n  --rekor-version VERSION\n                        Force the rekor transparency log version. Valid values\n                        are [1, 2]. By default the highest available version\n                        is used\n\nOpenID Connect options:\n  --identity-token TOKEN\n                        the OIDC identity token to use (default: None)\n  --oidc-client-id ID   The custom OpenID Connect client ID to use during\n                        OAuth2 (default: sigstore)\n  --oidc-client-secret SECRET\n                        The custom OpenID Connect client secret to use during\n                        OAuth2 (default: None)\n  --oidc-disable-ambient-providers\n                        Disable ambient OpenID Connect credential detection\n                        (e.g. on GitHub Actions) (default: False)\n  --oidc-issuer URL     The OpenID Connect issuer to use (default: None)\n  --oauth-force-oob     Force an out-of-band OAuth flow and do not\n                        automatically start the default web browser (default:\n                        False)\n\nOutput options:\n  --no-default-files    Don't emit the default output files\n                        ({input}.sigstore.json) (default: False)\n  --signature FILE, --output-signature FILE\n                        Write a single signature to the given file; does not\n                        work with multiple input files (default: None)\n  --certificate FILE, --output-certificate FILE\n                        Write a single certificate to the given file; does not\n                        work with multiple input files (default: None)\n  --bundle FILE         Write a single Sigstore bundle to the given file; does\n                        not work with multiple input files (default: None)\n  --output-directory DIR\n                        Write default outputs to the given directory\n                        (conflicts with --signature, --certificate, --bundle)\n                        (default: None)\n  --overwrite           Overwrite preexisting signature and certificate\n                        outputs, if present (default: False)\n```\n<!-- @end-sigstore-sign-help@ -->\n\n\n### Signing with DSSE envelopes\n\n<!-- @begin-sigstore-attest-help@ -->\n```\nusage: sigstore attest [-h] [-v] [--rekor-version VERSION] --predicate FILE\n                       --predicate-type TYPE [--identity-token TOKEN]\n                       [--oidc-client-id ID] [--oidc-client-secret SECRET]\n                       [--oidc-disable-ambient-providers] [--oidc-issuer URL]\n                       [--oauth-force-oob] [--bundle FILE] [--overwrite]\n                       FILE [FILE ...]\n\npositional arguments:\n  FILE                  The file to sign\n\noptions:\n  -h, --help            show this help message and exit\n  -v, --verbose         run with additional debug logging; supply multiple\n                        times to increase verbosity (default: 0)\n  --rekor-version VERSION\n                        Force the rekor transparency log version. Valid values\n                        are [1, 2]. By default the highest available version\n                        is used\n\nDSSE options:\n  --predicate FILE      Path to the predicate file (default: None)\n  --predicate-type TYPE\n                        Specify a predicate type\n                        (https://slsa.dev/provenance/v0.2,\n                        https://slsa.dev/provenance/v1) (default: None)\n\nOpenID Connect options:\n  --identity-token TOKEN\n                        the OIDC identity token to use (default: None)\n  --oidc-client-id ID   The custom OpenID Connect client ID to use during\n                        OAuth2 (default: sigstore)\n  --oidc-client-secret SECRET\n                        The custom OpenID Connect client secret to use during\n                        OAuth2 (default: None)\n  --oidc-disable-ambient-providers\n                        Disable ambient OpenID Connect credential detection\n                        (e.g. on GitHub Actions) (default: False)\n  --oidc-issuer URL     The OpenID Connect issuer to use (default: None)\n  --oauth-force-oob     Force an out-of-band OAuth flow and do not\n                        automatically start the default web browser (default:\n                        False)\n\nOutput options:\n  --bundle FILE         Write a single Sigstore bundle to the given file; does\n                        not work with multiple input files (default: None)\n  --overwrite           Overwrite preexisting bundle outputs, if present\n                        (default: False)\n```\n<!-- @end-sigstore-attest-help@ -->\n\n### Verifying\n\n#### Identities\n\n<!-- @begin-sigstore-verify-identity-help@ -->\n```\nusage: sigstore verify identity [-h] [-v] [--certificate FILE]\n                                [--signature FILE] [--bundle FILE] [--offline]\n                                --cert-identity IDENTITY --cert-oidc-issuer\n                                URL\n                                FILE_OR_DIGEST [FILE_OR_DIGEST ...]\n\noptions:\n  -h, --help            show this help message and exit\n  -v, --verbose         run with additional debug logging; supply multiple\n                        times to increase verbosity (default: 0)\n\nVerification inputs:\n  --certificate FILE, --cert FILE\n                        The PEM-encoded certificate to verify against; not\n                        used with multiple inputs (default: None)\n  --signature FILE      The signature to verify against; not used with\n                        multiple inputs (default: None)\n  --bundle FILE         The Sigstore bundle to verify with; not used with\n                        multiple inputs (default: None)\n  FILE_OR_DIGEST        The file path or the digest to verify. The digest\n                        should start with the 'sha256:' prefix.\n\nVerification options:\n  --offline             Perform offline verification; requires a Sigstore\n                        bundle (default: False)\n  --cert-identity IDENTITY\n                        The identity to check for in the certificate's Subject\n                        Alternative Name (default: None)\n  --cert-oidc-issuer URL\n                        The OIDC issuer URL to check for in the certificate's\n                        OIDC issuer extension (default: None)\n```\n<!-- @end-sigstore-verify-identity-help@ -->\n\n#### Signatures from GitHub Actions\n\n<!-- @begin-sigstore-verify-github-help@ -->\n```\nusage: sigstore verify github [-h] [-v] [--certificate FILE]\n                              [--signature FILE] [--bundle FILE] [--offline]\n                              [--cert-identity IDENTITY] [--trigger EVENT]\n                              [--sha SHA] [--name NAME] [--repository REPO]\n                              [--ref REF]\n                              FILE_OR_DIGEST [FILE_OR_DIGEST ...]\n\noptions:\n  -h, --help            show this help message and exit\n  -v, --verbose         run with additional debug logging; supply multiple\n                        times to increase verbosity (default: 0)\n\nVerification inputs:\n  --certificate FILE, --cert FILE\n                        The PEM-encoded certificate to verify against; not\n                        used with multiple inputs (default: None)\n  --signature FILE      The signature to verify against; not used with\n                        multiple inputs (default: None)\n  --bundle FILE         The Sigstore bundle to verify with; not used with\n                        multiple inputs (default: None)\n  FILE_OR_DIGEST        The file path or the digest to verify. The digest\n                        should start with the 'sha256:' prefix.\n\nVerification options:\n  --offline             Perform offline verification; requires a Sigstore\n                        bundle (default: False)\n  --cert-identity IDENTITY\n                        The identity to check for in the certificate's Subject\n                        Alternative Name (default: None)\n  --trigger EVENT       The GitHub Actions event name that triggered the\n                        workflow (default: None)\n  --sha SHA             The `git` commit SHA that the workflow run was invoked\n                        with (default: None)\n  --name NAME           The name of the workflow that was triggered (default:\n                        None)\n  --repository REPO     The repository slug that the workflow was triggered\n                        under (default: None)\n  --ref REF             The `git` ref that the workflow was invoked with\n                        (default: None)\n```\n<!-- @end-sigstore-verify-github-help@ -->\n\n## Troubleshooting\n\nFirst, please make sure you are using a recent and supported release:\nsigstore-python project provides support for the latest release and\nbest effort critical bug fixes for the latest 3.6.x release.\n\n### Common issues\n\n1. \"_bundle contains a transparency log entry that is incompatible with\n   this version of sigstore-python_\" (as well as \"_not enough sources of\n   verified time_\") means an upgrade is necessary to verify this signature\n   bundle: Signature bundles with Rekor v2 transparency log entries can only be\n   verified with sigstore-python 4 and above\n1. verifying without a network connection results in HTTP errors: By default\n   sigstore-python checks for updates to the trusted key material on every\n   startup. This can be avoided temporarily with `--offline` but please read the\n   [documentation](https://sigstore.github.io/sigstore-python/advanced/offline/)\n   for caveats\n1. Signing results in HTTP errors: Signing with sigstore-python depends on multiple\n   Sigstore services. Retrying on failure may be a useful workaround if any of\n   these services fail but filing issues for specific failures is appreciated\n\n### My problem is something else\n\nPlease [open an issue](https://github.com/sigstore/sigstore-python/issues/new?template=bug.md) or ask in the [slack channel](#community).\n\n## Documentation\n\n`sigstore` documentation is available on [https://sigstore.github.io/sigstore-python](https://sigstore.github.io/sigstore-python)\n\n## Licensing\n\n`sigstore` is licensed under the Apache 2.0 License.\n\n## Community\n\n`sigstore-python` is developed as part of the [Sigstore](https://sigstore.dev) project.\n\nWe also use a [Slack channel](https://sigstore.slack.com)!\nClick [here](https://join.slack.com/t/sigstore/shared_invite/zt-mhs55zh0-XmY3bcfWn4XEyMqUUutbUQ) for the invite link.\n\n## Contributing\n\nSee [the contributing docs](https://github.com/sigstore/.github/blob/main/CONTRIBUTING.md) for details.\n\n## Code of Conduct\n\nEveryone interacting with this project is expected to follow the\n[sigstore Code of Conduct](https://github.com/sigstore/.github/blob/main/CODE_OF_CONDUCT.md).\n\n## Security\n\nShould you discover any security issues, please refer to sigstore's [security\nprocess](https://github.com/sigstore/.github/blob/main/SECURITY.md).\n"
  },
  {
    "path": "cloudbuild.yaml",
    "content": "steps:\n  # Install dependencies\n  - name: python\n    entrypoint: python\n    args: [\"-m\", \"pip\", \"install\", \".\", \"--user\"]\n\n  # Sign with ambient GCP credentials\n  - name: python\n    entrypoint: python\n    args: [\"-m\", \"sigstore\", \"sign\", \"README.md\"]\n    env:\n      - \"GOOGLE_SERVICE_ACCOUNT_NAME=sigstore-python-test@projectsigstore.iam.gserviceaccount.com\"\n"
  },
  {
    "path": "docs/advanced/custom_trust.md",
    "content": "# Custom Sigstore instances\n\nBy default, `sigstore` is configured to work with the public `sigstore.dev`\ninstance. The trust materials for this instance are bundled with the client,\nallowing for a seamless out-of-the-box experience.\n\nIn addition to the public instance, `sigstore` also supports using custom\nSigstore instances. When using a custom instance, you are responsible\nfor providing the trust materials (at least once). This document outlines\nthe methods for doing so.\n\n### Using a custom instance\n\nUsing a custom Sigstore instance is a two-step process:\n\n1.  First, you must establish trust for the new instance. This is done using the\n    `sigstore trust-instance` command. This step only needs to be performed once.\n2.  Once trust is established, you can use the `--instance` flag with `sigstore`\n    commands like `sign` and `verify` to point to your custom instance.\n\nTo establish trust for a custom instance, you need its TUF root file. You can then run:\n\n```console\n$ sigstore --instance https://my-sigstore.example.com trust-instance my-root.json\n```\n\nAfter successfully adding the new instance, you can use it for signing and verifying\nartifacts. For example, to sign a file:\n\n```console\n$ sigstore --instance https://my-sigstore.example.com sign foo.txt\n```\n\n### Using a custom instance with local configuration\n\nThe trust configuration can also be provided as a local file -- but the user is now\nresponsible for keeping the trust configuration updated.\n\nThe `--trust-config` flag, accepts a JSON-formatted file conforming to the `ClientTrustConfig`\nmessage in the [Sigstore protobuf specs](https://github.com/sigstore/protobuf-specs).\nThis file configures the entire Sigstore instance state, *including* the URIs\nused to access the CA and artifact transparency services as well as the\ncryptographic root of trust itself.\n\nTo use a custom client config, prepend `--trust-config` to any `sigstore`\ncommand:\n\n```console\n$ sigstore --trust-config custom.trustconfig.json sign foo.txt\n$ sigstore --trust-config custom.trustconfig.json verify identity foo.txt ...\n```"
  },
  {
    "path": "docs/advanced/offline.md",
    "content": "# Offline Verification\n\n!!! danger \n    Because `--offline` disables trust root updates, `sigstore-python` falls back\n    to the latest cached trust root or, if none exists, the trust root baked\n    into `sigstore-python` itself. Like with any other offline verification,\n    this means that users may miss trust root changes (such as new root keys,\n    or revocations) unless they separately keep the trust root up-to-date.\n    \n    Users who need to operationalize offline verification may wish to do this\n    by distributing their own trust configuration; see\n    [Custom instance with local configuration](./custom_trust.md#using-a-custom-instance-with-local-configuration).\n\nDuring verification, there are two kinds of network access that `sigstore-python`\n*can* perform:\n\n1. When verifying against \"detached\" materials (e.g. separate `.crt` and `.sig`\n   files), `sigstore-python` can perform an online transparency log lookup.\n2. By default, during all verifications, `sigstore-python` will attempt to\n   refresh the locally cached root of trust via a TUF update.\n\nWhen performing bundle verification (i.e. `.sigstore` or `.sigstore.json`),\n(1) does not apply. However, (2) can still result in online accesses.\n\nTo perform **fully** offline verification, pass `--offline` to your\n`sigstore verify` subcommand:\n\n```bash\n$ sigstore verify identity foo.txt \\\n    --offline \\\n    --cert-identity 'hamilcar@example.com' \\\n    --cert-oidc-issuer 'https://github.com/login/oauth'\n```\n\nAlternatively, users may choose to bypass TUF entirely by passing\nan entire trust configuration to `sigstore-python` via `--trust-config`:\n\n```bash\n$ sigstore --trust-config public.trustconfig.json verify identity ...\n```\n\nThis will similarly result in fully offline operation, as the trust\nconfiguration contains a full trust root.\n"
  },
  {
    "path": "docs/api/errors.md",
    "content": ":::sigstore.errors\n "
  },
  {
    "path": "docs/api/hashes.md",
    "content": ":::sigstore.hashes\n "
  },
  {
    "path": "docs/api/index.md",
    "content": "!!! note\n\n    The API reference is automatically generated from the docstrings\n\n:::sigstore\n        "
  },
  {
    "path": "docs/api/models.md",
    "content": ":::sigstore.models\n "
  },
  {
    "path": "docs/api/oidc.md",
    "content": ":::sigstore.oidc\n "
  },
  {
    "path": "docs/api/sign.md",
    "content": ":::sigstore.sign\n "
  },
  {
    "path": "docs/api/verify/policy.md",
    "content": ":::sigstore.verify.policy\n "
  },
  {
    "path": "docs/api/verify/verifier.md",
    "content": ":::sigstore.verify.verifier\n "
  },
  {
    "path": "docs/index.md",
    "content": "# Home\n\n## Introduction\n\n`sigstore` is a Python tool for generating and verifying [Sigstore] signatures.\nYou can use it to sign and verify Python package distributions, or anything\nelse!\n\n## Features\n\n* Support for keyless signature generation and verification with [Sigstore](https://www.sigstore.dev/)\n* Support for signing with [\"ambient\" OpenID Connect identities](./signing.md#signing-with-ambient-credentials)\n* A comprehensive [CLI](#using-sigstore) and corresponding\n  [importable Python API](./api/index.md)\n\n## Installing `sigstore`\n\n```console\npython -m pip install sigstore\n```\n\nSee [installation](./installation.md) for more detailed installation instructions or options.\n\n## Using `sigstore`\n\nYou can run `sigstore` as a standalone program, or via `python -m`:\n\n```console\nsigstore --help\npython -m sigstore --help\n```\n\n- Use `sigstore` to [sign](./signing.md)\n- Use `sigstore` to [verify](./verify.md)\n\n## SLSA Provenance\n\nThis project emits a [SLSA] provenance on its release! This enables you to verify the integrity\nof the downloaded artifacts and ensured that the binary's code really comes from this source code.\n\nTo do so, please follow the instructions [here](https://github.com/slsa-framework/slsa-github-generator#verification-of-provenance).\n\n[SLSA]: https://slsa.dev/\n[Sigstore]: https://www.sigstore.dev/"
  },
  {
    "path": "docs/installation.md",
    "content": "# Installation\n\n## With `pip`\n\n`sigstore` requires Python 3.9 or newer, and can be installed directly via `pip`:\n\n```console\npython -m pip install sigstore\n```\n\nOptionally, 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:\n\n```console\npython -m pip install -r https://raw.githubusercontent.com/sigstore/sigstore-python/main/install/requirements.txt\n```\n\nThis installs the requirements file located [here](https://github.com/sigstore/sigstore-python/blob/main/install/requirements.txt), which is kept up-to-date.\n\n## With `uv`\n\n!!! warning\n    \n    `sigstore` depends on `betterproto` pre-releases versions, which are by default not resolved by `uv`.\n\n```console\nuv pip install --prerelease=allow sigstore\n```\n\n`sigstore` can also be used as tool:\n\n```console\nuvx --prerelease=allow sigstore --help\n```\n\n## GitHub Actions\n\n`sigstore-python` has [an official GitHub Action](https://github.com/sigstore/gh-action-sigstore-python)!\n\nYou can install it from the [GitHub Marketplace](https://github.com/marketplace/actions/gh-action-sigstore-python), or\nadd it to your CI manually:\n\n```yaml\njobs:\n  sigstore-python:\n    steps:\n      - uses: sigstore/gh-action-sigstore-python@v3.0.0\n        with:\n          inputs: foo.txt\n```\n\nSee the [action documentation](https://github.com/sigstore/gh-action-sigstore-python/blob/main/README.md) for more details and usage examples."
  },
  {
    "path": "docs/policy.md",
    "content": "# Policies\n\nThis document describes the set of policies followed by `sigstore-python` \nwhen signing or verifying a bundle.\n\n`sigstore-python` follows the [Sigstore: Client Spec] and this document \noutline mimic the one from the spec.\n\n## Signing\n\n### Authentication\n\n`sigstore-python` supports several authentication mechanisms :\n\n- An OAuth flow: this mode is preferred for interactive workflows.\n- An _ambient_ detection: this mode is preferred for un-attended workflows \n  (i.e., continuous integration system)\n\n### Key generation\n\n`sigstore-python` uses [ECDSA] as its signing algorithm.\n\n### Certificate Issuance\n\n_using Fulcio_\n\n### Signing\n\nWhen needed, the payload pre-hashing algorithm is `SHA2_256`.\n\n### Timestamping\n\nIf Timestamp Authorities have been provided in the Signing Config, a \nTimestamp Request using the hash of the signature is automatically sent to the \nprovided Timestamp Authorities.\n\nThis step allows to attest of the signature time.\n\n### Submission of Signing Metadata to Transparency Service\n\nThe Transparency Service, [rekor], is used by `sigstore-python` to provide a\npublic, immutable record of signing events. This step is crucial for ensuring\nthe integrity and transparency of the signing process.\n\n!!! warning\n\n    This step is performed before the `Timestamping` step in the workflow.\n\n### Signing Choices\n\nHere's a summary of the key choices in the `sigstore-python` signing process:\n\n| Option                        | `sigstore-python`            |\n|-------------------------------|------------------------------|\n| Digital signature algorithm   | ECDSA                        |\n| Signature metadata format     | ???                          |\n| Payload pre-hashing algorithm | SHA2 (256)                   |\n| Long-lived signing keys       | not used                     |\n| Timestamping                  | Used if provided             |\n| Transparency                  | Always used (rekor)          |\n| Other workflows               | no other workflows supported |\n\n## Verification\n\n`sigstore-python` supports configuring the verification process using policies\nbut this must be done using the [api](./api/index.md). By default, the CLI uses\nthe [`Identity`][sigstore.verify.policy] verification policy.\n\n### Establishing a Time for the Signature\n\nIf the bundle contains one or more signed times from Timestamping Authorities,\nthey will be used as the time source. In this case, a Timestamp Authority \nconfiguration must be provided in the `ClientTrustConfig`. When verifying \nTimestamp Authorities Responses, at least one must be valid.\n\nIf there is a Transparency Service Timestamp, this is also used as a source \nof trusted time.\n\nThe verification will fail if no sources of time are found.\n\n### Certificate\n\nFor a signature to be considered valid, it must meet two key criteria:\n\n- The signature must have an associated timestamp.\n- Every certificate in the chain, from the signing certificate up to the root\n  certificate, must be valid at the time of signing.\n\nThis approach is known as the “hybrid model” of certificate verification, as\ndescribed by [Braun et al.].\n\nThis validation process is repeated for each available source of trusted time.\nThe signature is only considered valid if it passes the validation checks\nagainst all of these time sources.\n\n#### SignedCertificateTimestamp\n\nThe `SignedCertificateTimestamp` is extracted from the leaf certificate and \nverified using the verification key from the Certificate Transparency Log.\n\n#### Identity Verification Policy\n\nThe system verifies that the signing certificate conforms to the Sigstore X. 509\nprofile as well as `Identity Policy`.\n\n### Transparency Log Entry\n\nThe Verifier now verifies the inclusion proof and signed checkpoint for the \nlog entry using [rekor].\n\nIf there is an inclusion promise, this is also verified.\n\n#### Time insertion check\n\nThe system verifies that the transparency log entry’s insertion timestamp falls\nwithin the certificate’s validity period.\n\nIf the insertion timestamp is outside the certificate’s validity period, it\ncould indicate potential backdating or use of an expired certificate, and the\nverification will fail.\n\n\n### Signature Verification\n\nThe next verification step is to verify the actual signature. This ensures\nthat the signed content has not been tampered with and was indeed signed by the\nclaimed entity.\n\nThe verification process differs slightly depending on the type of signed\ncontent:\n\n- DSSE: The entire envelope structure is used as the verification payload. \n- Artifacts: The raw bytes of the artifacts serve as the verification payload.\n\n#### Final step\n\nFinally, a last consistency check is performed to verify that the constructed \npayload is indeed the one that has been signed. This step is ussed to prevent\nvariants of [CVE-2022-36056]. \n\n[Sigstore: Client Spec]: https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit?usp=sharing\n[ECDSA]: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm\n[rekor]: https://github.com/sigstore/rekor\n[Braun et al.]: https://research.tue.nl/en/publications/how-to-avoid-the-breakdown-of-public-key-infrastructures-forward-\n[CVE-2022-36056]: https://github.com/sigstore/cosign/security/advisories/GHSA-8gw7-4j42-w388"
  },
  {
    "path": "docs/scripts/gen_ref_pages.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport argparse\nimport shutil\nimport sys\nfrom pathlib import Path\n\nroot = Path(__file__).parent.parent.parent\nsrc = root / \"sigstore\"\napi_root = root / \"docs\" / \"api\"\n\n\ndef main(args: argparse.Namespace) -> None:\n    \"\"\"Main script.\"\"\"\n    if args.overwrite:\n        shutil.rmtree(api_root, ignore_errors=True)\n    elif not args.check and api_root.exists():\n        print(f\"API root {api_root} already exists, skipping.\")\n        sys.exit(0)\n\n    seen = set()\n    for path in src.rglob(\"*.py\"):\n        module_path = path.relative_to(src).with_suffix(\"\")\n        full_doc_path = api_root / path.relative_to(src).with_suffix(\".md\")\n\n        # Exclude private entries\n        if any(part.startswith(\"_\") for part in module_path.parts):\n            continue\n\n        if args.check and not full_doc_path.is_file():\n            print(f\"File {full_doc_path} does not exist.\", file=sys.stderr)\n            sys.exit(1)\n\n        full_doc_path.parent.mkdir(parents=True, exist_ok=True)\n        with full_doc_path.open(\"w\") as f:\n            f.write(f\":::sigstore.{str(module_path).replace('/', '.')}\\n \")\n\n        seen.add(full_doc_path)\n\n    # Add the root\n    with (api_root / \"index.md\").open(\"w\") as f:\n        f.write(\"\"\"!!! note\n\n    The API reference is automatically generated from the docstrings\n\n:::sigstore\n        \"\"\")\n\n    seen.add(api_root / \"index.md\")\n\n    if args.check:\n        if diff := set(api_root.rglob(\"*.md\")).symmetric_difference(seen):\n            print(f\"Found leftover documentation file: {diff}\", file=sys.stderr)\n            sys.exit(1)\n    else:\n        print(\"API doc generated.\")\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description=\"Generate the structure for the API documentation.\"\n    )\n    parser.add_argument(\"--overwrite\", action=\"store_true\", default=False)\n    parser.add_argument(\"--check\", action=\"store_true\", default=False)\n\n    arguments = parser.parse_args()\n\n    if arguments.check and arguments.overwrite:\n        print(\"You can't specify both --check and --overwrite.\", file=sys.stderr)\n        sys.exit(1)\n\n    main(arguments)\n"
  },
  {
    "path": "docs/signing.md",
    "content": "# Signing\n\n!!! warning\n\n    By default signing an artifact creates a public record in `Rekor` which is publicly available.\n    The transparency log entry is browsable at `https://search.sigstore.dev/?logIndex=<LOG_INDEX>` \n    and disclose the signing identity.\n\n## Identities\n\n### Signing with ambient credentials\n\nFor environments that support OpenID Connect, `sigstore` supports ambient credential\ndetection. This includes many popular CI platforms and cloud providers. See the full list of\nsupported environments [here](https://github.com/di/id#supported-environments).\n\n### Signing with an email identity\n\n`sigstore` can use an OAuth2 + OpenID flow to establish an email identity,\nallowing you to request signing certificates that attest to control over\nthat email.\n\nBy default, `sigstore` attempts to do [ambient credential detection](#signing-with-ambient-credentials), which may preempt\nthe OAuth2 flow. To force the OAuth2 flow, you can explicitly disable ambient detection:\n\n```console\n$ sigstore sign --oidc-disable-ambient-providers foo.txt\n```\n\n### Signing with an explicit identity token\n\nIf you can't use an ambient credential or the OAuth2 flow, you can pass a pre-created\nidentity token directly into `sigstore sign`:\n\n```console\n$ sigstore sign --identity-token YOUR-LONG-JWT-HERE foo.txt\n```\n\nNote that passing a custom identity token does not circumvent Fulcio's requirements,\nnamely the Fulcio's supported identity providers and the claims expected within the token.\n\n!!! note\n\n    The examples in the section below are using ambient credential detection.\n    When no credentials are detected, it opens a browser to perform an interactive OAuth2 authentication flow.\n\n## Signing an artifact\n\nThe easiest option to sign an artifact with `sigstore` is to use the `sign` command.\n\nFor example, signing `sigstore-python` [README.md](https://github.com/sigstore/sigstore-python/blob/main/README.md).\n\n```console\n$ sigstore sign README.md\n\nWaiting for browser interaction...\nUsing ephemeral certificate:\n-----BEGIN CERTIFICATE-----\nMIIC2TCCAl+gAwIBAgIUdqkRnuxTr6bgdKtNiItu3+y8UkIwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQxMjEyMDk1NTU5WhcNMjQxMjEyMTAwNTU5WjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEjb33vsuuNr4phkmpkUvMB19rnXLtS9QqZGT+\nkDetyi9+wYv/g2oOFDfEm7UHPLUeZJ6Bad8Zd7H/JqGUhuJ7gaOCAX4wggF6MA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUJpNq\n0mPqLw1ypudG98REMY7mjyowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5j\nb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsG\nCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgor\nBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p\n7o4AAAGTukvv5QAABAMARzBFAiEA3oqdIinnZ9rGb7CTxQ60G6xi6l3T+z6vkSr2\nERAnIp4CIHbx61camOWU8dClH2WMUfguQ11+D82IQQBnHF968g22MAoGCCqGSM49\nBAMDA2gAMGUCMQDdf8S5Y/UhAp2vd2eo+RsjtfsasXSI51kO1ppNz42rSa6b5djW\n8+we6/OzVQW+THYCMBaBHPNntloKD040Pce6f8W3HpydbUzshJ24Emt/EaTPqH/g\ngYd2xz5hd4vQ7Ysmsg==\n-----END CERTIFICATE-----\n\nTransparency log entry created at index: 155016378\nMEQCIHVjH0I3iarhB5hD0MEE4AZ7GpCPZhXpdsVsSFlZIynVAiA10qzWt9FBC5pjD6+1kLRS14F+muVD1NJZNw6b+/WADQ==\nSigstore bundle written to README.md.sigstore.json\n \n```\n\nThe log entry is available at : [https://search.sigstore.dev/?logIndex=155016378](https://search.sigstore.dev/?logIndex=155016378)\n\n## Attest\n\n`sigstore` can be used to generate attestations for software artifacts using [SLSA].\n\n!!! info \"What is SLSA?\"\n    \n    Supply-chain Levels for Software Artifacts, or SLSA (\"salsa\").\n    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.\n\n\nAt the moment, `sigstore` supports the following predicates types:\n\n- [https://slsa.dev/provenance/v1](https://slsa.dev/spec/v1.0/provenance)\n- [https://slsa.dev/provenance/v0.2](https://slsa.dev/spec/v0.2/provenance)\n\nExample :\n\n```console\n$ sigstore attest \\         \n    --predicate-type \"https://slsa.dev/provenance/v1\" \\\n    --predicate ./test/assets/integration/attest/slsa_predicate_v1_0.json \\\n    ./README.md\n    \nWaiting for browser interaction...\nUsing ephemeral certificate:\n-----BEGIN CERTIFICATE-----\nMIIC2TCCAmCgAwIBAgIUI1GUnwGV69rXWAixrFmwAcZ7j7IwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQxMjEyMTAxODUwWhcNMjQxMjEyMTAyODUwWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEZPieQV37ByUyf+zWMGjXmom+kM4INxPcO1Kf\nDhjV3RmhTAlKOYXGU38O/KUNka5BLTb4f5r1bNwGhiEf9qcmNqOCAX8wggF7MA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUUexC\nqnLoKejMCAAgNxN77wSlIHkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5j\nb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsG\nCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGLBgor\nBgEEAdZ5AgQCBH0EewB5AHcA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p\n7o4AAAGTumDcJAAABAMASDBGAiEAprGPiBTcRK8ZFM+x3HLE+2s82xPAecHfJo9F\nRXNI+CMCIQCYzRBQtTehd+LLmwkXjPJEsJ5CpI7q1uDhhspyplVSLjAKBggqhkjO\nPQQDAwNnADBkAjAjO7BG9Gx6ggm1/IP75l+LzUnAP/DP0BOBeM0/lXZN3BBUvtdq\n+oTUzmmY/VpCWggCMEcCMn4UDIF/jBrVhES8ks57T8LjRX6xacpn9ufpkTlnKs6w\nS8/kL6jEREOcdnpOSQ==\n-----END CERTIFICATE-----\n\nTransparency log entry created at index: 155019253\nSigstore bundle written to README.md.sigstore.json\n```\n\n[SLSA]: https://slsa.dev/\n"
  },
  {
    "path": "docs/stylesheets/custom.css",
    "content": "/* From https://github.com/sigstore/community/blob/main/artwork/Sigstore_BrandGuide_March2023.pdf */\n:root {\n  --md-primary-fg-color:        #2e2f71;\n  --md-primary-bg-color:         #f9f7ef;\n}"
  },
  {
    "path": "docs/verify.md",
    "content": "# Verifying\n\n## Generic identities\n\nThis is the most common verification done with `sigstore`, and therefore\nthe one you probably want: you can use it to verify that a signature was\nproduced by a particular identity (like `hamilcar@example.com`), as attested\nto by a particular OIDC provider (like `https://github.com/login/oauth`).\n\n```console\n$ sigstore verify identity --cert-identity <IDENTITY> --cert-oidc-issuer <URL> FILE_OR_DIGEST\n```\n\nThe following command will verify that the bundle `tests/assets/bundle.txt.sigstore` was signed by `a@tny.town` using\nthe staging infrastructure of `sigstore`.\n\n```console\n$ sigstore --staging verify identity --cert-identity \"a@tny.town\" --cert-oidc-issuer \"https://github.com/login/oauth\" test/assets/bundle.txt\n```\n\n## Verifying from GitHub Actions\n\nIf your signatures are coming from GitHub Actions (e.g., a workflow that uses its [ambient credentials](./signing.md#signing-with-ambient-credentials)),\nthen you can use the `sigstore verify github` subcommand to verify\nclaims more precisely than `sigstore verify identity` allows.\n\n`sigstore verify github` can be used to verify claims specific to signatures coming from GitHub\nActions. `sigstore-python` signs releases via GitHub Actions, so the examples below are working\nexamples of how you can verify a given `sigstore-python` release.\n\nWhen using `sigstore verify github`, you must pass `--cert-identity` or `--repository`, or both.\nUnlike `sigstore verify identity`, `--cert-oidc-issuer` is **not** required (since it's\ninferred to be GitHub Actions).\n\nVerifying with `--cert-identity`:\n\n```console\n$ sigstore verify github sigstore-0.10.0-py3-none-any.whl \\\n    --bundle sigstore-0.10.0-py3-none-any.whl.bundle \\\n    --cert-identity https://github.com/sigstore/sigstore-python/.github/workflows/release.yml@refs/tags/v0.10.0\n```\n\nVerifying with `--repository`:\n\n```console\n$ sigstore verify github sigstore-0.10.0-py3-none-any.whl \\\n    --bundle sigstore-0.10.0-py3-none-any.whl.bundle \\\n    --repository sigstore/sigstore-python\n```\n\nAdditional GitHub Actions specific claims can be verified like so:\n\n```console\n$ sigstore verify github sigstore-0.10.0-py3-none-any.whl \\\n    --bundle sigstore-0.10.0-py3-none-any.whl.bundle \\\n    --cert-identity https://github.com/sigstore/sigstore-python/.github/workflows/release.yml@refs/tags/v0.10.0 \\\n    --trigger release \\\n    --sha 66581529803929c3ccc45334632ccd90f06e0de4 \\\n    --name Release \\\n    --repository sigstore/sigstore-python \\\n    --ref refs/tags/v0.10.0\n```\n\n## Verifying against a bundle\n\nBy default, `sigstore verify identity` will attempt to find a `<filename>.sigstore.json`\nor `<filename>.sigstore` in the same directory as the file being verified:\n\n```console\n# looks for foo.txt.sigstore.json\n$ sigstore verify identity foo.txt \\\n    --cert-identity 'hamilcar@example.com' \\\n    --cert-oidc-issuer 'https://github.com/login/oauth'\n```\n\nMultiple files can be verified at once:\n\n```console\n# looks for {foo,bar}.txt.sigstore.json\n$ python -m sigstore verify identity foo.txt bar.txt \\\n    --cert-identity 'hamilcar@example.com' \\\n    --cert-oidc-issuer 'https://github.com/login/oauth'\n```\n\n## Verifying a digest instead of a file\n\n`sigstore-python` supports verifying digests directly, without requiring the artifact to be\npresent. The digest should be prefixed with the `sha256:` string:\n\n```console\n$ sigstore verify identity sha256:ce8ab2822671752e201ea1e19e8c85e73d497e1c315bfd9c25f380b7625d1691 \\\n    --cert-identity 'hamilcar@example.com' \\\n    --cert-oidc-issuer 'https://github.com/login/oauth'\n    --bundle 'foo.txt.sigstore.json'\n```"
  },
  {
    "path": "install/requirements.in",
    "content": "sigstore==4.2.0\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "# yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json\n\nsite_name: sigstore-python\nsite_url: https://sigstore.github.io/sigstore-python\nrepo_url: https://github.com/sigstore/sigstore-python\nsite_description: sigstore-python, a Sigstore client written in Python\nrepo_name: sigstore-python\nedit_uri: edit/main/docs/\ntheme:\n  name: material\n  icon:\n    repo: fontawesome/brands/github\n  logo: assets/images/logo.png\n  features:\n    - content.action.edit\n    - content.code.copy\n    - header.autohide\n    - navigation.instant\n    - navigation.instant.progress\n    - navigation.footer\n    - search.highlight\n    - search.suggest\n  palette:\n    primary: custom\n  font:\n    text: Inter\nextra_css:\n  - stylesheets/custom.css\nnav:\n  - Home: index.md\n  - Installation: installation.md\n  - Signing: signing.md\n  - Verifying: verify.md\n  - Policy: policy.md\n  - Advanced:\n      - Custom Sigstore Instances: advanced/custom_trust.md\n      - Offline Verification: advanced/offline.md\n  # begin-api-section\n  - API:\n      - api/index.md\n      - Models: api/models.md\n      - Errors: api/errors.md\n      - Hashes: api/hashes.md\n      - OIDC: api/oidc.md\n      - Sign: api/sign.md\n      - Verify:\n          - Policy: api/verify/policy.md\n          - Verifier: api/verify/verifier.md\n  # end-api-section\nmarkdown_extensions:\n  - admonition\n  - pymdownx.details\n  - pymdownx.superfences\ncopyright: sigstore &copy; 2024\nplugins:\n  - search\n  - social\n  - mkdocstrings:\n      handlers:\n        python:\n          options:\n            members_order: source\n            unwrap_annotated: true\n            modernize_annotations: true\n            merge_init_into_class: true\n            docstring_section_style: spacy\n            signature_crossrefs: true\n            show_symbol_type_toc: true\n            filters:\n              - '!^_'\nvalidation:\n  omitted_files: warn\n  unrecognized_links: warn\n  anchors: warn\n  not_found: warn\n\nextra:\n  generator: false\n  social:\n    - icon: fontawesome/brands/slack\n      link: https://sigstore.slack.com\n    - icon: fontawesome/brands/x-twitter\n      link: https://twitter.com/projectsigstore\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"flit_core >=3.2,<4\"]\nbuild-backend = \"flit_core.buildapi\"\n\n[project]\nname = \"sigstore\"\ndynamic = [\"version\"]\ndescription = \"A tool for signing Python package distributions\"\nreadme = \"README.md\"\nlicense = { file = \"LICENSE\" }\nauthors = [\n  { name = \"Sigstore Authors\", email = \"sigstore-dev@googlegroups.com\" },\n]\nclassifiers = [\n  \"License :: OSI Approved :: Apache Software License\",\n  \"Programming Language :: Python :: 3 :: Only\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n  \"Development Status :: 5 - Production/Stable\",\n  \"Intended Audience :: Developers\",\n  \"Topic :: Security\",\n  \"Topic :: Security :: Cryptography\",\n]\ndependencies = [\n  \"cryptography >= 42, < 47\",\n  \"id >= 1.1.0\",\n  \"importlib_resources ~= 5.7; python_version < '3.11'\",\n  \"pyasn1 ~= 0.6\",\n  \"pydantic >= 2,< 3\",\n  \"pyjwt >= 2.1\",\n  \"pyOpenSSL >= 23.0.0\",\n  \"requests\",\n  \"rich >= 13,< 16\",\n  \"rfc8785 ~= 0.1.2\",\n  \"rfc3161-client >= 1.0.3,< 1.1.0\",\n  # Both sigstore-models and sigstore-rekor types are unstable\n  # so we pin them conservatively.\n  \"sigstore-models == 0.0.6\",\n  \"sigstore-rekor-types == 0.0.18\",\n  \"tuf ~= 6.0\",\n  \"platformdirs ~= 4.2\",\n]\nrequires-python = \">=3.10\"\n\n[project.scripts]\nsigstore = \"sigstore._cli:main\"\n\n[project.urls]\nHomepage = \"https://pypi.org/project/sigstore/\"\nIssues = \"https://github.com/sigstore/sigstore-python/issues\"\nSource = \"https://github.com/sigstore/sigstore-python\"\nDocumentation = \"https://sigstore.github.io/sigstore-python/\"\n\n[project.optional-dependencies]\ntest = [\"pytest\", \"pytest-cov\", \"pretend\", \"coverage[toml]\"]\nlint = [\n  \"bandit\",\n  # \"interrogate >= 1.7.0\",\n  \"mypy ~= 1.1\",\n  # NOTE(ww): ruff is under active development, so we pin conservatively here\n  # and let Dependabot periodically perform this update.\n  \"ruff < 0.15.12\",\n  \"types-requests\",\n  \"types-pyOpenSSL\",\n]\ndoc = [\"mkdocs-material[imaging]\", \"mkdocstrings-python\"]\ndev = [\"build\", \"bump >= 1.3.2\", \"sigstore[doc,test,lint]\"]\n\n[tool.coverage.run]\n# branch coverage in addition to statement coverage.\nbranch = true\n# FIXME(jl): currently overridden. see: https://pytest-cov.readthedocs.io/en/latest/config.html\n# include machine name, process id, and a random number in `.coverage-*` so each file is distinct.\nparallel = true\n# store relative path info for aggregation across runs with potentially differing filesystem layouts.\n# see: https://coverage.readthedocs.io/en/7.1.0/config.html#config-run-relative-files\nrelative_files = true\n# don't attempt code coverage for the CLI entrypoints\nomit = [\"sigstore/_cli.py\"]\n\n[tool.coverage.report]\nexclude_lines = [\n  \"@abc.abstractmethod\",\n  \"@typing.overload\",\n  \"if typing.TYPE_CHECKING\",\n]\n\n[tool.interrogate]\n# don't enforce documentation coverage for packaging, testing, the virtual\n# environment, or the CLI (which is documented separately).\nexclude = [\"env\", \"test\", \"sigstore/_cli.py\"]\nignore-semiprivate = true\nignore-private = true\n# Ignore nested classes for docstring coverage because we use them primarily\n# for pydantic model configuration.\nignore-nested-classes = true\nfail-under = 100\n\n[tool.mypy]\nallow_redefinition = true\ncheck_untyped_defs = true\ndisallow_incomplete_defs = true\ndisallow_untyped_defs = true\nenable_error_code = [\"ignore-without-code\", \"redundant-expr\", \"truthy-bool\"]\nignore_missing_imports = true\nno_implicit_optional = true\nsqlite_cache = true\nstrict = true\nstrict_equality = true\nwarn_no_return = true\nwarn_redundant_casts = true\nwarn_return_any = true\nwarn_unreachable = true\nwarn_unused_configs = true\nwarn_unused_ignores = true\nplugins = [\"pydantic.mypy\"]\n\n[tool.bandit]\nexclude_dirs = [\"./test\"]\n\n[tool.ruff.lint]\nextend-select = [\"I\", \"UP\"]\nignore = [\n  \"UP007\", # https://github.com/pydantic/pydantic/issues/4146\n  \"UP011\",\n  \"UP015\",\n]\n"
  },
  {
    "path": "sigstore/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nThe `sigstore` Python APIs.\n\nFor command-line usage of `sigstore`, refer to the `sigstore`\n[README](https://github.com/sigstore/sigstore-python).\n\nOtherwise, here are some quick starting points:\n\n* `sigstore.verify`: verifying of Sigstore signatures,\n  including flexible policy control\n* `sigstore.sign`: creation of Sigstore signatures\n\"\"\"\n\n__version__ = \"4.2.0\"\n"
  },
  {
    "path": "sigstore/__main__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nThe `python -m sigstore` entrypoint.\n\"\"\"\n\nif __name__ == \"__main__\":  # pragma: no cover\n    from sigstore._cli import main\n\n    main()\n"
  },
  {
    "path": "sigstore/_cli.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom __future__ import annotations\n\nimport argparse\nimport base64\nimport json\nimport logging\nimport os\nimport sys\nfrom concurrent import futures\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any, NoReturn, TypeAlias, Union\n\nfrom cryptography.hazmat.primitives.serialization import Encoding\nfrom cryptography.x509 import load_pem_x509_certificate\nfrom pydantic import ValidationError\nfrom rich.console import Console\nfrom rich.logging import RichHandler\nfrom sigstore_models.bundle.v1 import Bundle as RawBundle\nfrom sigstore_models.common.v1 import HashAlgorithm\n\nfrom sigstore import __version__, dsse\nfrom sigstore._internal.fulcio.client import ExpiredCertificate\nfrom sigstore._internal.rekor import _hashedrekord_from_parts\nfrom sigstore._internal.rekor.client import RekorClient\nfrom sigstore._utils import sha256_digest\nfrom sigstore.dsse import StatementBuilder, Subject\nfrom sigstore.dsse._predicate import (\n    PredicateType,\n    SLSAPredicateV0_2,\n    SLSAPredicateV1_0,\n)\nfrom sigstore.errors import CertValidationError, Error, VerificationError\nfrom sigstore.hashes import Hashed\nfrom sigstore.models import Bundle, ClientTrustConfig, InvalidBundle\nfrom sigstore.oidc import (\n    ExpiredIdentity,\n    IdentityToken,\n    Issuer,\n    detect_credential,\n)\nfrom sigstore.sign import Signer, SigningContext\nfrom sigstore.verify import (\n    Verifier,\n    policy,\n)\n\n_console = Console(file=sys.stderr)\nlogging.basicConfig(\n    format=\"%(message)s\", datefmt=\"[%X]\", handlers=[RichHandler(console=_console)]\n)\n_logger = logging.getLogger(__name__)\n\n# NOTE: We configure the top package logger, rather than the root logger,\n# to avoid overly verbose logging in third-party code by default.\n_package_logger = logging.getLogger(\"sigstore\")\n_package_logger.setLevel(os.environ.get(\"SIGSTORE_LOGLEVEL\", \"INFO\").upper())\n\n\n@dataclass(frozen=True)\nclass SigningOutputs:\n    signature: Path | None = None\n    certificate: Path | None = None\n    bundle: Path | None = None\n\n\n@dataclass(frozen=True)\nclass VerificationUnbundledMaterials:\n    certificate: Path\n    signature: Path\n\n\n@dataclass(frozen=True)\nclass VerificationBundledMaterials:\n    bundle: Path\n\n\nVerificationMaterials: TypeAlias = Union[\n    VerificationUnbundledMaterials, VerificationBundledMaterials\n]\n\n# Map of inputs -> outputs for signing operations\nOutputMap: TypeAlias = dict[Path, SigningOutputs]\n\n\ndef _fatal(message: str) -> NoReturn:\n    \"\"\"\n    Logs a fatal condition and exits.\n    \"\"\"\n    _logger.fatal(message)\n    sys.exit(1)\n\n\ndef _invalid_arguments(args: argparse.Namespace, message: str) -> NoReturn:\n    \"\"\"\n    An `argparse` helper that fixes up the type hints on our use of\n    `ArgumentParser.error`.\n    \"\"\"\n    args._parser.error(message)\n    raise ValueError(\"unreachable\")\n\n\ndef _boolify_env(envvar: str) -> bool:\n    \"\"\"\n    An `argparse` helper for turning an environment variable into a boolean.\n\n    The semantics here closely mirror `distutils.util.strtobool`.\n\n    See: <https://docs.python.org/3/distutils/apiref.html#distutils.util.strtobool>\n    \"\"\"\n    val = os.getenv(envvar)\n    if val is None:\n        return False\n\n    val = val.lower()\n    if val in {\"y\", \"yes\", \"true\", \"t\", \"on\", \"1\"}:\n        return True\n    elif val in {\"n\", \"no\", \"false\", \"f\", \"off\", \"0\"}:\n        return False\n    else:\n        raise ValueError(f\"can't coerce '{val}' to a boolean\")\n\n\ndef _add_shared_verify_input_options(group: argparse._ArgumentGroup) -> None:\n    \"\"\"\n    Common input options, shared between all `sigstore verify` subcommands.\n    \"\"\"\n    group.add_argument(\n        \"--certificate\",\n        \"--cert\",\n        metavar=\"FILE\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_CERTIFICATE\"),\n        help=\"The PEM-encoded certificate to verify against; not used with multiple inputs\",\n    )\n    group.add_argument(\n        \"--signature\",\n        metavar=\"FILE\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_SIGNATURE\"),\n        help=\"The signature to verify against; not used with multiple inputs\",\n    )\n    group.add_argument(\n        \"--bundle\",\n        metavar=\"FILE\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_BUNDLE\"),\n        help=(\"The Sigstore bundle to verify with; not used with multiple inputs\"),\n    )\n\n    def file_or_digest(arg: str) -> Hashed | Path:\n        path = Path(arg)\n        if path.is_file():\n            return path\n        elif arg.startswith(\"sha256\"):\n            digest = bytes.fromhex(arg[len(\"sha256:\") :])\n            if len(digest) != 32:\n                raise ValueError\n            return Hashed(\n                digest=digest,\n                algorithm=HashAlgorithm.SHA2_256,\n            )\n        else:\n            raise ValueError\n\n    group.add_argument(\n        \"files_or_digest\",\n        metavar=\"FILE_OR_DIGEST\",\n        type=file_or_digest,\n        nargs=\"+\",\n        help=\"The file path or the digest to verify. The digest should start with the 'sha256:' prefix.\",\n    )\n\n\ndef _add_shared_verification_options(group: argparse._ArgumentGroup) -> None:\n    group.add_argument(\n        \"--offline\",\n        action=\"store_true\",\n        default=_boolify_env(\"SIGSTORE_OFFLINE\"),\n        help=\"Perform offline verification; requires a Sigstore bundle\",\n    )\n\n\ndef _add_shared_oidc_options(\n    group: argparse._ArgumentGroup | argparse.ArgumentParser,\n) -> None:\n    \"\"\"\n    Common OIDC options, shared between `sigstore sign` and `sigstore get-identity-token`.\n    \"\"\"\n    group.add_argument(\n        \"--oidc-client-id\",\n        metavar=\"ID\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_OIDC_CLIENT_ID\", \"sigstore\"),\n        help=\"The custom OpenID Connect client ID to use during OAuth2\",\n    )\n    group.add_argument(\n        \"--oidc-client-secret\",\n        metavar=\"SECRET\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_OIDC_CLIENT_SECRET\"),\n        help=\"The custom OpenID Connect client secret to use during OAuth2\",\n    )\n    group.add_argument(\n        \"--oidc-disable-ambient-providers\",\n        action=\"store_true\",\n        default=_boolify_env(\"SIGSTORE_OIDC_DISABLE_AMBIENT_PROVIDERS\"),\n        help=\"Disable ambient OpenID Connect credential detection (e.g. on GitHub Actions)\",\n    )\n    group.add_argument(\n        \"--oidc-issuer\",\n        metavar=\"URL\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_OIDC_ISSUER\", None),\n        help=\"The OpenID Connect issuer to use\",\n    )\n    group.add_argument(\n        \"--oauth-force-oob\",\n        action=\"store_true\",\n        default=_boolify_env(\"SIGSTORE_OAUTH_FORCE_OOB\"),\n        help=\"Force an out-of-band OAuth flow and do not automatically start the default web browser\",\n    )\n\n\ndef _parser() -> argparse.ArgumentParser:\n    # Arguments in parent_parser can be used for both commands and subcommands\n    parent_parser = argparse.ArgumentParser(add_help=False)\n    parent_parser.add_argument(\n        \"-v\",\n        \"--verbose\",\n        action=\"count\",\n        default=0,\n        help=\"run with additional debug logging; supply multiple times to increase verbosity\",\n    )\n\n    parser = argparse.ArgumentParser(\n        prog=\"sigstore\",\n        description=\"a tool for signing and verifying Python package distributions\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    parser.add_argument(\n        \"-V\", \"--version\", action=\"version\", version=f\"sigstore {__version__}\"\n    )\n\n    global_instance_options = parser.add_mutually_exclusive_group()\n    global_instance_options.add_argument(\n        \"--staging\",\n        action=\"store_true\",\n        default=_boolify_env(\"SIGSTORE_STAGING\"),\n        help=(\n            \"Use sigstore's staging instance, instead of the default production instance.\"\n            \" Mutually exclusive with other instance configuration arguments.\"\n        ),\n    )\n    global_instance_options.add_argument(\n        \"--instance\",\n        metavar=\"URL\",\n        type=str,\n        help=(\n            \"Use a given Sigstore instance URL, instead of the default production instance.\"\n            \" Mutually exclusive with other instance configuration arguments.\"\n        ),\n    )\n    global_instance_options.add_argument(\n        \"--trust-config\",\n        metavar=\"FILE\",\n        type=Path,\n        help=(\n            \"Use given client trust configuration instead of using the default production\"\n            \" instance. Mutually exclusive with other instance configuration arguments.\"\n        ),\n    )\n    subcommands = parser.add_subparsers(\n        required=True,\n        dest=\"subcommand\",\n        metavar=\"COMMAND\",\n        help=\"the operation to perform\",\n    )\n\n    # `sigstore attest`\n    attest = subcommands.add_parser(\n        \"attest\",\n        help=\"sign one or more inputs using DSSE\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    attest.add_argument(\n        \"--rekor-version\",\n        type=int,\n        metavar=\"VERSION\",\n        default=argparse.SUPPRESS,\n        help=\"Force the rekor transparency log version. Valid values are [1, 2]. By default the highest available version is used\",\n    )\n    attest.add_argument(\n        \"files\",\n        metavar=\"FILE\",\n        type=Path,\n        nargs=\"+\",\n        help=\"The file to sign\",\n    )\n\n    dsse_options = attest.add_argument_group(\"DSSE options\")\n    dsse_options.add_argument(\n        \"--predicate\",\n        metavar=\"FILE\",\n        type=Path,\n        required=True,\n        help=\"Path to the predicate file\",\n    )\n    dsse_options.add_argument(\n        \"--predicate-type\",\n        metavar=\"TYPE\",\n        choices=list(PredicateType),\n        type=PredicateType,\n        required=True,\n        help=f\"Specify a predicate type ({', '.join(list(PredicateType))})\",\n    )\n\n    oidc_options = attest.add_argument_group(\"OpenID Connect options\")\n    oidc_options.add_argument(\n        \"--identity-token\",\n        metavar=\"TOKEN\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_IDENTITY_TOKEN\"),\n        help=\"the OIDC identity token to use\",\n    )\n    _add_shared_oidc_options(oidc_options)\n\n    output_options = attest.add_argument_group(\"Output options\")\n    output_options.add_argument(\n        \"--bundle\",\n        metavar=\"FILE\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_BUNDLE\"),\n        help=(\n            \"Write a single Sigstore bundle to the given file; does not work with multiple input \"\n            \"files\"\n        ),\n    )\n    output_options.add_argument(\n        \"--overwrite\",\n        action=\"store_true\",\n        default=_boolify_env(\"SIGSTORE_OVERWRITE\"),\n        help=\"Overwrite preexisting bundle outputs, if present\",\n    )\n\n    # `sigstore sign`\n    sign = subcommands.add_parser(\n        \"sign\",\n        help=\"sign one or more inputs\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    sign.add_argument(\n        \"--rekor-version\",\n        type=int,\n        metavar=\"VERSION\",\n        default=argparse.SUPPRESS,\n        help=\"Force the rekor transparency log version. Valid values are [1, 2]. By default the highest available version is used\",\n    )\n\n    oidc_options = sign.add_argument_group(\"OpenID Connect options\")\n    oidc_options.add_argument(\n        \"--identity-token\",\n        metavar=\"TOKEN\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_IDENTITY_TOKEN\"),\n        help=\"the OIDC identity token to use\",\n    )\n    _add_shared_oidc_options(oidc_options)\n\n    output_options = sign.add_argument_group(\"Output options\")\n    output_options.add_argument(\n        \"--no-default-files\",\n        action=\"store_true\",\n        default=_boolify_env(\"SIGSTORE_NO_DEFAULT_FILES\"),\n        help=\"Don't emit the default output files ({input}.sigstore.json)\",\n    )\n    output_options.add_argument(\n        \"--signature\",\n        \"--output-signature\",\n        metavar=\"FILE\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_OUTPUT_SIGNATURE\"),\n        help=(\n            \"Write a single signature to the given file; does not work with multiple input files\"\n        ),\n    )\n    output_options.add_argument(\n        \"--certificate\",\n        \"--output-certificate\",\n        metavar=\"FILE\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_OUTPUT_CERTIFICATE\"),\n        help=(\n            \"Write a single certificate to the given file; does not work with multiple input files\"\n        ),\n    )\n    output_options.add_argument(\n        \"--bundle\",\n        metavar=\"FILE\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_BUNDLE\"),\n        help=(\n            \"Write a single Sigstore bundle to the given file; does not work with multiple input \"\n            \"files\"\n        ),\n    )\n    output_options.add_argument(\n        \"--output-directory\",\n        metavar=\"DIR\",\n        type=Path,\n        default=os.getenv(\"SIGSTORE_OUTPUT_DIRECTORY\"),\n        help=(\n            \"Write default outputs to the given directory (conflicts with --signature, --certificate\"\n            \", --bundle)\"\n        ),\n    )\n    output_options.add_argument(\n        \"--overwrite\",\n        action=\"store_true\",\n        default=_boolify_env(\"SIGSTORE_OVERWRITE\"),\n        help=\"Overwrite preexisting signature and certificate outputs, if present\",\n    )\n\n    sign.add_argument(\n        \"files\",\n        metavar=\"FILE\",\n        type=Path,\n        nargs=\"+\",\n        help=\"The file to sign\",\n    )\n\n    # `sigstore verify`\n    verify = subcommands.add_parser(\n        \"verify\",\n        help=\"verify one or more inputs\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    verify_subcommand = verify.add_subparsers(\n        required=True,\n        dest=\"verify_subcommand\",\n        metavar=\"COMMAND\",\n        help=\"the kind of verification to perform\",\n    )\n\n    # `sigstore verify identity`\n    verify_identity = verify_subcommand.add_parser(\n        \"identity\",\n        help=\"verify against a known identity and identity provider\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    input_options = verify_identity.add_argument_group(\"Verification inputs\")\n    _add_shared_verify_input_options(input_options)\n\n    verification_options = verify_identity.add_argument_group(\"Verification options\")\n    _add_shared_verification_options(verification_options)\n    verification_options.add_argument(\n        \"--cert-identity\",\n        metavar=\"IDENTITY\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_CERT_IDENTITY\"),\n        help=\"The identity to check for in the certificate's Subject Alternative Name\",\n        required=True,\n    )\n    verification_options.add_argument(\n        \"--cert-oidc-issuer\",\n        metavar=\"URL\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_CERT_OIDC_ISSUER\"),\n        help=\"The OIDC issuer URL to check for in the certificate's OIDC issuer extension\",\n        required=True,\n    )\n\n    # `sigstore verify github`\n    verify_github = verify_subcommand.add_parser(\n        \"github\",\n        help=\"verify against GitHub Actions-specific claims\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n\n    input_options = verify_github.add_argument_group(\"Verification inputs\")\n    _add_shared_verify_input_options(input_options)\n\n    verification_options = verify_github.add_argument_group(\"Verification options\")\n    _add_shared_verification_options(verification_options)\n    verification_options.add_argument(\n        \"--cert-identity\",\n        metavar=\"IDENTITY\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_CERT_IDENTITY\"),\n        help=\"The identity to check for in the certificate's Subject Alternative Name\",\n    )\n    verification_options.add_argument(\n        \"--trigger\",\n        dest=\"workflow_trigger\",\n        metavar=\"EVENT\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_VERIFY_GITHUB_WORKFLOW_TRIGGER\"),\n        help=\"The GitHub Actions event name that triggered the workflow\",\n    )\n    verification_options.add_argument(\n        \"--sha\",\n        dest=\"workflow_sha\",\n        metavar=\"SHA\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_VERIFY_GITHUB_WORKFLOW_SHA\"),\n        help=\"The `git` commit SHA that the workflow run was invoked with\",\n    )\n    verification_options.add_argument(\n        \"--name\",\n        dest=\"workflow_name\",\n        metavar=\"NAME\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_VERIFY_GITHUB_WORKFLOW_NAME\"),\n        help=\"The name of the workflow that was triggered\",\n    )\n    verification_options.add_argument(\n        \"--repository\",\n        dest=\"workflow_repository\",\n        metavar=\"REPO\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_VERIFY_GITHUB_WORKFLOW_REPOSITORY\"),\n        help=\"The repository slug that the workflow was triggered under\",\n    )\n    verification_options.add_argument(\n        \"--ref\",\n        dest=\"workflow_ref\",\n        metavar=\"REF\",\n        type=str,\n        default=os.getenv(\"SIGSTORE_VERIFY_GITHUB_WORKFLOW_REF\"),\n        help=\"The `git` ref that the workflow was invoked with\",\n    )\n\n    # `sigstore get-identity-token`\n    get_identity_token = subcommands.add_parser(\n        \"get-identity-token\",\n        help=\"retrieve and return a Sigstore-compatible OpenID Connect token\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    _add_shared_oidc_options(get_identity_token)\n\n    # `sigstore trust-instance`\n    trust_instance = subcommands.add_parser(\n        \"trust-instance\",\n        help=\"Initialize trust for a Sigstore instance\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    trust_instance.add_argument(\n        \"root\",\n        metavar=\"ROOT\",\n        type=Path,\n        help=\"The TUF root metadata for the instance\",\n    )\n\n    # `sigstore plumbing`\n    plumbing = subcommands.add_parser(\n        \"plumbing\",\n        help=\"developer-only plumbing operations\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    plumbing_subcommands = plumbing.add_subparsers(\n        required=True,\n        dest=\"plumbing_subcommand\",\n        metavar=\"COMMAND\",\n        help=\"the operation to perform\",\n    )\n\n    # `sigstore plumbing fix-bundle`\n    fix_bundle = plumbing_subcommands.add_parser(\n        \"fix-bundle\",\n        help=\"fix (and optionally upgrade) older bundle formats\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n    fix_bundle.add_argument(\n        \"--bundle\",\n        metavar=\"FILE\",\n        type=Path,\n        required=True,\n        help=(\"The bundle to fix and/or upgrade\"),\n    )\n    fix_bundle.add_argument(\n        \"--upgrade-version\",\n        action=\"store_true\",\n        help=\"Upgrade the bundle to the latest bundle spec version\",\n    )\n    fix_bundle.add_argument(\n        \"--in-place\",\n        action=\"store_true\",\n        help=\"Overwrite the input bundle with its fix instead of emitting to stdout\",\n    )\n\n    # `sigstore plumbing update-trust-root`\n    plumbing_subcommands.add_parser(\n        \"update-trust-root\",\n        help=\"update the local trust root to the latest version via TUF\",\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n        parents=[parent_parser],\n    )\n\n    return parser\n\n\ndef main(args: list[str] | None = None) -> None:\n    if not args:\n        args = sys.argv[1:]\n\n    parser = _parser()\n    args = parser.parse_args(args)\n\n    # Configure logging upfront, so that we don't miss anything.\n    if args.verbose >= 1:\n        _package_logger.setLevel(\"DEBUG\")\n    if args.verbose >= 2:\n        logging.getLogger().setLevel(\"DEBUG\")\n\n    _logger.debug(f\"parsed arguments {args}\")\n\n    # Stuff the parser back into our namespace, so that we can use it for\n    # error handling later.\n    args._parser = parser\n\n    try:\n        if args.subcommand == \"sign\":\n            _sign(args)\n        elif args.subcommand == \"attest\":\n            _attest(args)\n        elif args.subcommand == \"verify\":\n            if args.verify_subcommand == \"identity\":\n                _verify_identity(args)\n            elif args.verify_subcommand == \"github\":\n                _verify_github(args)\n        elif args.subcommand == \"get-identity-token\":\n            _get_identity_token(args)\n        elif args.subcommand == \"trust-instance\":\n            _trust_instance(args)\n        elif args.subcommand == \"plumbing\":\n            if args.plumbing_subcommand == \"fix-bundle\":\n                _fix_bundle(args)\n            elif args.plumbing_subcommand == \"update-trust-root\":\n                _update_trust_root(args)\n        else:\n            _invalid_arguments(args, f\"Unknown subcommand: {args.subcommand}\")\n    except Error as e:\n        e.log_and_exit(_logger, args.verbose >= 1)\n\n\ndef _trust_instance(args: argparse.Namespace) -> None:\n    \"\"\"\n    Initialize trust for a Sigstore instance\n    \"\"\"\n    root: Path = args.root\n    instance: str | None = args.instance\n    if not root.is_file():\n        _invalid_arguments(args, f\"Input must be a file: {root}\")\n    if instance is None:\n        _invalid_arguments(args, \"trust-instance requires '--instance URL'\")\n\n    # ClientTrustConfig construction verifies the root is valid, and\n    # stores it in the local metadata store for future use\n    _ = ClientTrustConfig.from_tuf(instance, bootstrap_root=root)\n\n\ndef _get_identity_token(args: argparse.Namespace) -> None:\n    \"\"\"\n    Output the OIDC authentication token\n    \"\"\"\n    identity = _get_identity(args, _get_trust_config(args))\n    if identity:\n        print(identity)\n    else:\n        _invalid_arguments(args, \"No identity token supplied or detected!\")\n\n\ndef _sign_file_threaded(\n    signer: Signer,\n    predicate_type: str | None,\n    predicate: dict[str, Any] | None,\n    file: Path,\n    outputs: SigningOutputs,\n) -> None:\n    \"\"\"sign method to be called from signing thread\"\"\"\n    _logger.debug(f\"signing for {file.name}\")\n    with file.open(mode=\"rb\") as io:\n        # The input can be indefinitely large, so we perform a streaming\n        # digest and sign the prehash rather than buffering it fully.\n        digest = sha256_digest(io)\n    try:\n        if predicate is None:\n            result = signer.sign_artifact(input_=digest)\n        else:\n            subject = Subject(name=file.name, digest={\"sha256\": digest.digest.hex()})\n            statement_builder = StatementBuilder(\n                subjects=[subject],\n                predicate_type=predicate_type,\n                predicate=predicate,\n            )\n            result = signer.sign_dsse(statement_builder.build())\n    except ExpiredIdentity as exp_identity:\n        _logger.error(\"Signature failed: identity token has expired\")\n        raise exp_identity\n\n    except ExpiredCertificate as exp_certificate:\n        _logger.error(\"Signature failed: Fulcio signing certificate has expired\")\n        raise exp_certificate\n\n    _logger.info(\n        f\"Transparency log entry created at index: {result.log_entry._inner.log_index}\"\n    )\n\n    if outputs.signature is not None:\n        signature = base64.b64encode(result.signature).decode()\n        with outputs.signature.open(mode=\"w\", encoding=\"utf-8\") as io:\n            print(signature, file=io)\n\n    if outputs.certificate is not None:\n        cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode()\n        with outputs.certificate.open(mode=\"w\", encoding=\"utf-8\") as io:\n            print(cert_pem, file=io)\n\n    if outputs.bundle is not None:\n        with outputs.bundle.open(mode=\"w\", encoding=\"utf-8\") as io:\n            print(result.to_json(), file=io)\n\n\ndef _sign_common(\n    args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None\n) -> None:\n    \"\"\"\n    Signing logic for both `sigstore sign` and `sigstore attest`\n\n    Both `sign` and `attest` share the same signing logic, the only change is\n    whether they sign over a DSSE envelope or a hashedrekord.\n    This function differentiates between the two using the `predicate` argument. If\n    present, it will generate an in-toto statement and wrap it in a DSSE envelope. If\n    not, it will use a hashedrekord.\n    \"\"\"\n    # Select the signing context to use.\n    trust_config = _get_trust_config(args)\n    signing_ctx = SigningContext.from_trust_config(trust_config)\n\n    # The order of precedence for identities is as follows:\n    #\n    # 1) Explicitly supplied identity token\n    # 2) Ambient credential detected in the environment, unless disabled\n    # 3) Interactive OAuth flow\n    identity: IdentityToken | None\n    if args.identity_token:\n        identity = IdentityToken(args.identity_token, args.oidc_client_id)\n    else:\n        identity = _get_identity(args, trust_config)\n\n    if not identity:\n        _invalid_arguments(args, \"No identity token supplied or detected!\")\n\n    # Not all commands provide --predicate-type\n    predicate_type = getattr(args, \"predicate_type\", None)\n\n    with signing_ctx.signer(identity) as signer:\n        print(\"Using ephemeral certificate:\")\n        cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode()\n        print(cert_pem)\n\n        # sign in threads: this is relevant for especially Rekor v2 as otherwise we wait\n        # for log inclusion for each signature separately\n        with futures.ThreadPoolExecutor() as executor:\n            jobs = [\n                executor.submit(\n                    _sign_file_threaded,\n                    signer,\n                    predicate_type,\n                    predicate,\n                    file,\n                    outputs,\n                )\n                for file, outputs in output_map.items()\n            ]\n            for job in futures.as_completed(jobs):\n                job.result()\n\n        for file, outputs in output_map.items():\n            if outputs.signature is not None:\n                print(f\"Signature written to {outputs.signature}\")\n            if outputs.certificate is not None:\n                print(f\"Certificate written to {outputs.certificate}\")\n            if outputs.bundle is not None:\n                print(f\"Sigstore bundle written to {outputs.bundle}\")\n\n\ndef _attest(args: argparse.Namespace) -> None:\n    predicate_path = args.predicate\n    if not predicate_path.is_file():\n        _invalid_arguments(args, f\"Predicate must be a file: {predicate_path}\")\n\n    try:\n        with open(predicate_path, \"r\", encoding=\"utf-8\") as f:\n            predicate = json.load(f)\n            # We do a basic sanity check using our Pydantic models to see if the\n            # contents of the predicate file match the specified predicate type.\n            # Since most of the predicate fields are optional, this only checks that\n            # the fields that are present and correctly spelled have the expected\n            # type.\n            if args.predicate_type == PredicateType.SLSA_v0_2:\n                SLSAPredicateV0_2.model_validate(predicate)\n            elif args.predicate_type == PredicateType.SLSA_v1_0:\n                SLSAPredicateV1_0.model_validate(predicate)\n            else:\n                _invalid_arguments(\n                    args,\n                    f'Unsupported predicate type \"{args.predicate_type}\". Predicate type must be one of: {list(PredicateType)}',\n                )\n\n    except (ValidationError, json.JSONDecodeError) as e:\n        _invalid_arguments(\n            args, f'Unable to parse predicate of type \"{args.predicate_type}\": {e}'\n        )\n\n    # Build up the map of inputs -> outputs ahead of any signing operations,\n    # so that we can fail early if overwriting without `--overwrite`.\n    output_map: OutputMap = {}\n    for file in args.files:\n        if not file.is_file():\n            _invalid_arguments(args, f\"Input must be a file: {file}\")\n\n        bundle = args.bundle\n        output_dir = file.parent\n\n        if not bundle:\n            bundle = output_dir / f\"{file.name}.sigstore.json\"\n\n        if bundle and bundle.exists() and not args.overwrite:\n            _invalid_arguments(\n                args,\n                f\"Refusing to overwrite outputs without --overwrite: {bundle}\",\n            )\n        output_map[file] = SigningOutputs(bundle=bundle)\n\n    # We sign the contents of the predicate file, rather than signing the Pydantic\n    # model's JSON dump. This is because doing a JSON -> Model -> JSON roundtrip might\n    # change the original predicate if it doesn't match exactly our Pydantic model\n    # (e.g.: if it has extra fields).\n    _sign_common(args, output_map=output_map, predicate=predicate)\n\n\ndef _sign(args: argparse.Namespace) -> None:\n    has_sig = bool(args.signature)\n    has_crt = bool(args.certificate)\n    has_bundle = bool(args.bundle)\n\n    # `--no-default-files` has no effect on `--bundle`, but we forbid it because\n    # it indicates user confusion.\n    if args.no_default_files and has_bundle:\n        _invalid_arguments(\n            args, \"--no-default-files may not be combined with --bundle.\"\n        )\n\n    # Fail if `--signature` or `--certificate` is specified *and* we have more\n    # than one input.\n    if (has_sig or has_crt or has_bundle) and len(args.files) > 1:\n        _invalid_arguments(\n            args,\n            \"Error: --signature, --certificate, and --bundle can't be used with \"\n            \"explicit outputs for multiple inputs.\",\n        )\n\n    if args.output_directory and (has_sig or has_crt or has_bundle):\n        _invalid_arguments(\n            args,\n            \"Error: --signature, --certificate, and --bundle can't be used with \"\n            \"an explicit output directory.\",\n        )\n\n    # Fail if either `--signature` or `--certificate` is specified, but not both.\n    if has_sig ^ has_crt:\n        _invalid_arguments(\n            args, \"Error: --signature and --certificate must be used together.\"\n        )\n\n    # Build up the map of inputs -> outputs ahead of any signing operations,\n    # so that we can fail early if overwriting without `--overwrite`.\n    output_map: OutputMap = {}\n    for file in args.files:\n        if not file.is_file():\n            _invalid_arguments(args, f\"Input must be a file: {file}\")\n\n        sig, cert, bundle = (\n            args.signature,\n            args.certificate,\n            args.bundle,\n        )\n\n        output_dir = args.output_directory or file.parent\n        if output_dir.exists() and not output_dir.is_dir():\n            _invalid_arguments(\n                args, f\"Output directory exists and is not a directory: {output_dir}\"\n            )\n        output_dir.mkdir(parents=True, exist_ok=True)\n\n        if not bundle and not args.no_default_files:\n            bundle = output_dir / f\"{file.name}.sigstore.json\"\n\n        if not args.overwrite:\n            extants = []\n            if sig and sig.exists():\n                extants.append(str(sig))\n            if cert and cert.exists():\n                extants.append(str(cert))\n            if bundle and bundle.exists():\n                extants.append(str(bundle))\n\n            if extants:\n                _invalid_arguments(\n                    args,\n                    \"Refusing to overwrite outputs without --overwrite: \"\n                    f\"{', '.join(extants)}\",\n                )\n\n        output_map[file] = SigningOutputs(\n            signature=sig, certificate=cert, bundle=bundle\n        )\n\n    _sign_common(args, output_map=output_map, predicate=None)\n\n\ndef _collect_verification_state(\n    args: argparse.Namespace,\n) -> tuple[Verifier, list[tuple[Path | Hashed, Hashed, Bundle]]]:\n    \"\"\"\n    Performs CLI functionality common across all `sigstore verify` subcommands.\n\n    Returns a tuple of the active verifier instance and a list of `(path, hashed, bundle)`\n    tuples, where `path` is the filename for display purposes, `hashed` is the\n    pre-hashed input to the file being verified and `bundle` is the `Bundle` to verify with.\n    \"\"\"\n\n    # Fail if --certificate, --signature, or --bundle is specified, and we\n    # have more than one input.\n    if (args.certificate or args.signature or args.bundle) and len(\n        args.files_or_digest\n    ) > 1:\n        _invalid_arguments(\n            args,\n            \"--certificate, --signature, or --bundle can only be used \"\n            \"with a single input file or digest\",\n        )\n\n    # Fail if `--certificate` or `--signature` is used with `--bundle`.\n    if args.bundle and (args.certificate or args.signature):\n        _invalid_arguments(\n            args, \"--bundle cannot be used with --certificate or --signature\"\n        )\n\n    # Fail if digest input is not used with `--bundle` or both `--certificate` and `--signature`.\n    if any(isinstance(x, Hashed) for x in args.files_or_digest):\n        if not args.bundle and not (args.certificate and args.signature):\n            _invalid_arguments(\n                args,\n                \"verifying a digest input (sha256:*) needs either --bundle or both --certificate and --signature\",\n            )\n\n    # Fail if `--certificate` or `--signature` is used with `--offline`.\n    if args.offline and (args.certificate or args.signature):\n        _invalid_arguments(\n            args, \"--offline cannot be used with --certificate or --signature\"\n        )\n\n    # The converse of `sign`: we build up an expected input map and check\n    # that we have everything so that we can fail early.\n    input_map: dict[Path | Hashed, VerificationMaterials] = {}\n    for file in (f for f in args.files_or_digest if isinstance(f, Path)):\n        if not file.is_file():\n            _invalid_arguments(args, f\"Input must be a file: {file}\")\n\n        sig, cert, bundle = (\n            args.signature,\n            args.certificate,\n            args.bundle,\n        )\n        if sig is None:\n            sig = file.parent / f\"{file.name}.sig\"\n        if cert is None:\n            cert = file.parent / f\"{file.name}.crt\"\n        if bundle is None:\n            # NOTE(ww): If the user hasn't specified a bundle via `--bundle` and\n            # `{input}.sigstore.json` doesn't exist, then we try `{input}.sigstore`\n            # for backwards compatibility.\n            legacy_default_bundle = file.parent / f\"{file.name}.sigstore\"\n            bundle = file.parent / f\"{file.name}.sigstore.json\"\n\n            if not bundle.is_file() and legacy_default_bundle.is_file():\n                if not cert.is_file() or not sig.is_file():\n                    # NOTE(ww): Only show this warning if bare materials\n                    # are not provided, since bare materials take precedence over\n                    # a .sigstore bundle.\n                    _logger.warning(\n                        f\"{file}: {legacy_default_bundle} should be named {bundle}. \"\n                        \"Support for discovering 'bare' .sigstore inputs will be deprecated in \"\n                        \"a future release.\"\n                    )\n                bundle = legacy_default_bundle\n            elif bundle.is_file() and legacy_default_bundle.is_file():\n                # Don't allow the user to implicitly verify `{input}.sigstore.json` if\n                # `{input}.sigstore` is also present, since this implies user confusion.\n                _invalid_arguments(\n                    args,\n                    f\"Conflicting inputs: {bundle} and {legacy_default_bundle}\",\n                )\n\n        missing = []\n        if args.signature or args.certificate:\n            if not sig.is_file():\n                missing.append(str(sig))\n            if not cert.is_file():\n                missing.append(str(cert))\n            input_map[file] = VerificationUnbundledMaterials(\n                certificate=cert, signature=sig\n            )\n        else:\n            # If a user hasn't explicitly supplied `--signature` or `--certificate`,\n            # we expect a bundle either supplied via `--bundle` or with the\n            # default `{input}.sigstore(.json)?` name.\n            if not bundle.is_file():\n                missing.append(str(bundle))\n\n            input_map[file] = VerificationBundledMaterials(bundle=bundle)\n\n        if missing:\n            _invalid_arguments(\n                args,\n                f\"Missing verification materials for {(file)}: {', '.join(missing)}\",\n            )\n\n    if not input_map:\n        if len(args.files_or_digest) != 1:\n            # This should never happen, since if `input_map` is empty that means there\n            # were no file inputs, and therefore exactly one digest input should be\n            # present.\n            _invalid_arguments(\n                args, \"Internal error: Found multiple digests in CLI arguments\"\n            )\n        hashed = args.files_or_digest[0]\n        sig, cert, bundle = (\n            args.signature,\n            args.certificate,\n            args.bundle,\n        )\n        missing = []\n        if args.signature or args.certificate:\n            if not sig.is_file():\n                missing.append(str(sig))\n            if not cert.is_file():\n                missing.append(str(cert))\n            input_map[hashed] = VerificationUnbundledMaterials(\n                certificate=cert, signature=sig\n            )\n        else:\n            # If a user hasn't explicitly supplied `--signature` or `--certificate`,\n            # we expect a bundle supplied via `--bundle`\n            if not bundle.is_file():\n                missing.append(str(bundle))\n\n            input_map[hashed] = VerificationBundledMaterials(bundle=bundle)\n\n        if missing:\n            _invalid_arguments(\n                args,\n                f\"Missing verification materials for {(hashed)}: {', '.join(missing)}\",\n            )\n\n    trust_config = _get_trust_config(args)\n    verifier = Verifier(trusted_root=trust_config.trusted_root)\n\n    all_materials = []\n    for file_or_hashed, materials in input_map.items():\n        if isinstance(file_or_hashed, Path):\n            with file_or_hashed.open(mode=\"rb\") as io:\n                hashed = sha256_digest(io)\n        else:\n            hashed = file_or_hashed\n\n        if isinstance(materials, VerificationBundledMaterials):\n            # Load the bundle\n            _logger.debug(f\"Using bundle from: {materials.bundle}\")\n\n            bundle_bytes = materials.bundle.read_bytes()\n            bundle = Bundle.from_json(bundle_bytes)\n        else:\n            # Load the signing certificate\n            _logger.debug(f\"Using certificate from: {materials.certificate}\")\n            cert = load_pem_x509_certificate(materials.certificate.read_bytes())\n\n            # Load the signature\n            _logger.debug(f\"Using signature from: {materials.signature}\")\n            b64_signature = materials.signature.read_text(encoding=\"utf-8\")\n            signature = base64.b64decode(b64_signature)\n\n            # When using \"detached\" materials, we *must* retrieve the log\n            # entry from the online log.\n            # TODO: This should be abstracted somewhere much better.\n            log_entry = verifier._rekor.log.entries.retrieve.post(\n                _hashedrekord_from_parts(cert, signature, hashed)\n            )\n            if log_entry is None:\n                _invalid_arguments(\n                    args,\n                    f\"No matching log entry for {file_or_hashed}'s verification materials\",\n                )\n            bundle = Bundle.from_parts(cert, signature, log_entry)\n\n        _logger.debug(f\"Verifying contents from: {file_or_hashed}\")\n\n        all_materials.append((file_or_hashed, hashed, bundle))\n\n    return (verifier, all_materials)\n\n\ndef _verify_identity(args: argparse.Namespace) -> None:\n    verifier, materials = _collect_verification_state(args)\n\n    for file_or_digest, hashed, bundle in materials:\n        policy_ = policy.Identity(\n            identity=args.cert_identity,\n            issuer=args.cert_oidc_issuer,\n        )\n\n        try:\n            statement = _verify_common(verifier, hashed, bundle, policy_)\n            print(f\"OK: {file_or_digest}\", file=sys.stderr)\n            if statement is not None:\n                print(statement._contents.decode())\n        except Error as exc:\n            if isinstance(exc, CertValidationError):\n                _logger.warning(\n                    \"A certificate chain was not valid, are you using the correct Sigstore instance?\"\n                )\n\n            _logger.error(f\"FAIL: {file_or_digest}\")\n            exc.log_and_exit(_logger, args.verbose >= 1)\n\n\ndef _verify_github(args: argparse.Namespace) -> None:\n    inner_policies: list[policy.VerificationPolicy] = []\n\n    # We require at least one of `--cert-identity` or `--repository`,\n    # to minimize the risk of user confusion about what's being verified.\n    if not (args.cert_identity or args.workflow_repository):\n        _invalid_arguments(args, \"--cert-identity or --repository is required\")\n\n    # No matter what the user configures above, we require the OIDC issuer to\n    # be GitHub Actions.\n    inner_policies.append(\n        policy.OIDCIssuer(\"https://token.actions.githubusercontent.com\")\n    )\n\n    if args.cert_identity:\n        inner_policies.append(\n            policy.Identity(\n                identity=args.cert_identity,\n                # We always explicitly check the issuer below, so configuring\n                # it here is unnecessary.\n                issuer=None,\n            )\n        )\n    if args.workflow_trigger:\n        inner_policies.append(policy.GitHubWorkflowTrigger(args.workflow_trigger))\n    if args.workflow_sha:\n        inner_policies.append(policy.GitHubWorkflowSHA(args.workflow_sha))\n    if args.workflow_name:\n        inner_policies.append(policy.GitHubWorkflowName(args.workflow_name))\n    if args.workflow_repository:\n        inner_policies.append(policy.GitHubWorkflowRepository(args.workflow_repository))\n    if args.workflow_ref:\n        inner_policies.append(policy.GitHubWorkflowRef(args.workflow_ref))\n\n    policy_ = policy.AllOf(inner_policies)\n\n    verifier, materials = _collect_verification_state(args)\n    for file_or_digest, hashed, bundle in materials:\n        try:\n            statement = _verify_common(verifier, hashed, bundle, policy_)\n            print(f\"OK: {file_or_digest}\", file=sys.stderr)\n            if statement is not None:\n                print(statement._contents)\n        except Error as exc:\n            if isinstance(exc, CertValidationError):\n                _logger.warning(\n                    \"A certificate chain was not valid, are you using the correct Sigstore instance?\"\n                )\n\n            _logger.error(f\"FAIL: {file_or_digest}\")\n            exc.log_and_exit(_logger, args.verbose >= 1)\n\n\ndef _verify_common(\n    verifier: Verifier,\n    hashed: Hashed,\n    bundle: Bundle,\n    policy_: policy.VerificationPolicy,\n) -> dsse.Statement | None:\n    \"\"\"\n    Common verification handling.\n\n    This dispatches to either artifact or DSSE verification, depending on\n    `bundle`'s inner type.\n    If verifying a DSSE envelope, return the wrapped in-toto statement if\n    verification succeeds\n    \"\"\"\n\n    # If the bundle specifies a DSSE envelope, perform DSSE verification\n    # and assert that the inner payload is an in-toto statement bound\n    # to a subject matching the input's digest.\n    if bundle._dsse_envelope:\n        type_, payload = verifier.verify_dsse(bundle=bundle, policy=policy_)\n        if type_ != dsse.Envelope._TYPE:\n            raise VerificationError(f\"expected JSON payload for DSSE, got {type_}\")\n\n        stmt = dsse.Statement(payload)\n        if not stmt._matches_digest(hashed):\n            raise VerificationError(\n                f\"in-toto statement has no subject for digest {hashed.digest.hex()}\"\n            )\n        return stmt\n    else:\n        verifier.verify_artifact(\n            input_=hashed,\n            bundle=bundle,\n            policy=policy_,\n        )\n        return None\n\n\ndef _get_trust_config(args: argparse.Namespace) -> ClientTrustConfig:\n    \"\"\"\n    Return the client trust configuration (Sigstore service URLs, key material and lifetimes)\n\n    The configuration may come from explicit argument (--trust-config) or from the TUF\n    repository of the used Sigstore instance (--staging or --instance).\n    \"\"\"\n    # Not all commands provide --offline\n    offline = getattr(args, \"offline\", False)\n\n    if args.trust_config:\n        trust_config = ClientTrustConfig.from_json(args.trust_config.read_text())\n    elif args.instance:\n        trust_config = ClientTrustConfig.from_tuf(args.instance, offline=offline)\n    elif args.staging:\n        trust_config = ClientTrustConfig.staging(offline=offline)\n    else:\n        trust_config = ClientTrustConfig.production(offline=offline)\n\n    # Enforce rekor version if --rekor-version is used\n    trust_config.force_tlog_version = getattr(args, \"rekor_version\", None)\n\n    return trust_config\n\n\ndef _get_identity(\n    args: argparse.Namespace, trust_config: ClientTrustConfig\n) -> IdentityToken | None:\n    token = None\n    if not args.oidc_disable_ambient_providers:\n        token = detect_credential(args.oidc_client_id)\n\n    # Happy path: we've detected an ambient credential, so we can return early.\n    if token:\n        return IdentityToken(token, args.oidc_client_id)\n\n    if args.oidc_issuer is not None:\n        issuer = Issuer(args.oidc_issuer)\n    else:\n        issuer = Issuer(trust_config.signing_config.get_oidc_url())\n\n    if args.oidc_client_secret is None:\n        args.oidc_client_secret = \"\"  # nosec: B105\n\n    token = issuer.identity_token(\n        client_id=args.oidc_client_id,\n        client_secret=args.oidc_client_secret,\n        force_oob=args.oauth_force_oob,\n    )\n\n    return token\n\n\ndef _fix_bundle(args: argparse.Namespace) -> None:\n    # NOTE: We could support `--trusted-root` here in the future,\n    # for custom Rekor instances.\n\n    rekor = RekorClient.staging() if args.staging else RekorClient.production()\n\n    raw_bundle = RawBundle.from_json(args.bundle.read_bytes())\n\n    if len(raw_bundle.verification_material.tlog_entries) != 1:\n        _fatal(\"unfixable bundle: must have exactly one log entry\")\n\n    # Some old versions of sigstore-python (1.x) produce malformed\n    # bundles where the inclusion proof is present but without\n    # its checkpoint. We fix these by retrieving the complete entry\n    # from Rekor and replacing the incomplete entry.\n    tlog_entry = raw_bundle.verification_material.tlog_entries[0]\n    inclusion_proof = tlog_entry.inclusion_proof\n    if not inclusion_proof.checkpoint:\n        _logger.info(\"fixable: bundle's log entry is missing a checkpoint\")\n        new_entry = rekor.log.entries.get(log_index=tlog_entry.log_index)\n        raw_bundle.verification_material.tlog_entries = [new_entry._inner]\n\n    # Try to create our invariant-preserving Bundle from the any changes above.\n    try:\n        bundle = Bundle(raw_bundle)\n    except InvalidBundle as e:\n        e.log_and_exit(_logger)\n\n    # Round-trip through the bundle's parts to induce a version upgrade,\n    # if requested.\n    if args.upgrade_version:\n        bundle = Bundle._from_parts(*bundle._to_parts())\n\n    if args.in_place:\n        args.bundle.write_text(bundle.to_json())\n    else:\n        print(bundle.to_json())\n\n\ndef _update_trust_root(args: argparse.Namespace) -> None:\n    # Simply creating the TrustConfig in online mode is enough to perform\n    # a metadata update.\n\n    config = _get_trust_config(args)\n    _console.print(\n        f\"Trust root & signing config updated: {len(config.trusted_root.get_fulcio_certs())} Fulcio certificates\"\n    )\n"
  },
  {
    "path": "sigstore/_internal/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nsigstore-python's internal APIs.\n\nEverything in these APIs is considered internal and unstable, and is not\nsubject to any stability guarantees.\n\"\"\"\n\nfrom requests import __version__ as requests_version\n\nfrom sigstore import __version__ as sigstore_version\n\nUSER_AGENT = f\"sigstore-python/{sigstore_version} (python-requests/{requests_version})\"\n"
  },
  {
    "path": "sigstore/_internal/fulcio/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nAPIs for interacting with Fulcio.\n\"\"\"\n\nfrom .client import (\n    ExpiredCertificate,\n    FulcioCertificateSigningResponse,\n    FulcioClient,\n)\n\n__all__ = [\n    \"ExpiredCertificate\",\n    \"FulcioCertificateSigningResponse\",\n    \"FulcioClient\",\n]\n"
  },
  {
    "path": "sigstore/_internal/fulcio/client.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nClient implementation for interacting with Fulcio.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport json\nimport logging\nfrom abc import ABC\nfrom dataclasses import dataclass\nfrom urllib.parse import urljoin\n\nimport requests\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.x509 import (\n    Certificate,\n    CertificateSigningRequest,\n    load_pem_x509_certificate,\n)\n\nfrom sigstore._internal import USER_AGENT\nfrom sigstore._utils import B64Str\nfrom sigstore.oidc import IdentityToken\n\n_logger = logging.getLogger(__name__)\n\nSIGNING_CERT_ENDPOINT = \"/api/v2/signingCert\"\nTRUST_BUNDLE_ENDPOINT = \"/api/v2/trustBundle\"\n\n\nclass ExpiredCertificate(Exception):\n    \"\"\"An error raised when the Certificate is expired.\"\"\"\n\n\n@dataclass(frozen=True)\nclass FulcioCertificateSigningResponse:\n    \"\"\"Certificate response\"\"\"\n\n    cert: Certificate\n    chain: list[Certificate]\n\n\n@dataclass(frozen=True)\nclass FulcioTrustBundleResponse:\n    \"\"\"Trust bundle response, containing a list of certificate chains\"\"\"\n\n    trust_bundle: list[list[Certificate]]\n\n\nclass FulcioClientError(Exception):\n    \"\"\"\n    Raised on any error in the Fulcio client.\n    \"\"\"\n\n    pass\n\n\nclass _Endpoint(ABC):\n    def __init__(self, url: str, session: requests.Session) -> None:\n        self.url = url\n        self.session = session\n\n\ndef _serialize_cert_request(req: CertificateSigningRequest) -> str:\n    data = {\n        \"certificateSigningRequest\": B64Str(\n            base64.b64encode(req.public_bytes(serialization.Encoding.PEM)).decode()\n        )\n    }\n    return json.dumps(data)\n\n\nclass FulcioSigningCert(_Endpoint):\n    \"\"\"\n    Fulcio REST API signing certificate functionality.\n    \"\"\"\n\n    def post(\n        self, req: CertificateSigningRequest, identity: IdentityToken\n    ) -> FulcioCertificateSigningResponse:\n        \"\"\"\n        Get the signing certificate, using an X.509 Certificate\n        Signing Request.\n        \"\"\"\n        headers = {\n            \"Authorization\": f\"Bearer {identity}\",\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/pem-certificate-chain\",\n        }\n        resp: requests.Response = self.session.post(\n            url=self.url, data=_serialize_cert_request(req), headers=headers\n        )\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            # See if we can optionally add a message\n            if http_error.response:\n                text = json.loads(http_error.response.text)\n                if \"message\" in http_error.response.text:\n                    raise FulcioClientError(text[\"message\"]) from http_error\n            raise FulcioClientError from http_error\n\n        try:\n            certificates = resp.json()[\"signedCertificateEmbeddedSct\"][\"chain\"][\n                \"certificates\"\n            ]\n        except KeyError:\n            raise FulcioClientError(\"Fulcio response missing certificate chain\")\n\n        # Cryptography doesn't have chain verification/building built in\n        # https://github.com/pyca/cryptography/issues/2381\n        if len(certificates) < 2:\n            raise FulcioClientError(\n                f\"Certificate chain is too short: {len(certificates)} < 2\"\n            )\n        cert = load_pem_x509_certificate(certificates[0].encode())\n        chain = [load_pem_x509_certificate(c.encode()) for c in certificates[1:]]\n\n        return FulcioCertificateSigningResponse(cert, chain)\n\n\nclass FulcioTrustBundle(_Endpoint):\n    \"\"\"\n    Fulcio REST API trust bundle functionality.\n    \"\"\"\n\n    def get(self) -> FulcioTrustBundleResponse:\n        \"\"\"Get the certificate chains from Fulcio\"\"\"\n        resp: requests.Response = self.session.get(self.url)\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            raise FulcioClientError from http_error\n\n        trust_bundle_json = resp.json()\n        chains: list[list[Certificate]] = []\n        for certificate_chain in trust_bundle_json[\"chains\"]:\n            chain: list[Certificate] = []\n            for certificate in certificate_chain[\"certificates\"]:\n                cert: Certificate = load_pem_x509_certificate(certificate.encode())\n                chain.append(cert)\n            chains.append(chain)\n        return FulcioTrustBundleResponse(chains)\n\n\nclass FulcioClient:\n    \"\"\"The internal Fulcio client\"\"\"\n\n    def __init__(self, url: str) -> None:\n        \"\"\"Initialize the client\"\"\"\n        _logger.debug(f\"Fulcio client using URL: {url}\")\n        self.url = url\n        self.session = requests.Session()\n        self.session.headers.update(\n            {\n                \"User-Agent\": USER_AGENT,\n            }\n        )\n\n    def __del__(self) -> None:\n        \"\"\"\n        Destroys the underlying network session.\n        \"\"\"\n        self.session.close()\n\n    @property\n    def signing_cert(self) -> FulcioSigningCert:\n        \"\"\"\n        Returns a model capable of interacting with Fulcio's signing certificate endpoints.\n        \"\"\"\n        return FulcioSigningCert(\n            urljoin(self.url, SIGNING_CERT_ENDPOINT), session=self.session\n        )\n\n    @property\n    def trust_bundle(self) -> FulcioTrustBundle:\n        \"\"\"\n        Returns a model capable of interacting with Fulcio's trust bundle endpoints.\n        \"\"\"\n        return FulcioTrustBundle(\n            urljoin(self.url, TRUST_BUNDLE_ENDPOINT), session=self.session\n        )\n"
  },
  {
    "path": "sigstore/_internal/key_details.py",
    "content": "# Copyright 2025 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nUtilities for getting PublicKeyDetails.\n\"\"\"\n\nfrom cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa\nfrom cryptography.x509 import Certificate\nfrom sigstore_models.common.v1 import PublicKeyDetails\n\n\ndef _get_key_details(certificate: Certificate) -> PublicKeyDetails:\n    \"\"\"\n    Determine PublicKeyDetails from the Certificate.\n    We disclude the unrecommended types.\n    See\n    - https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md.\n    - https://github.com/sigstore/protobuf-specs/blob/3aaae418f76fb4b34df4def4cd093c464f20fed3/protos/sigstore_common.proto\n    \"\"\"\n    public_key = certificate.public_key()\n    params = certificate.signature_algorithm_parameters\n    if isinstance(public_key, ec.EllipticCurvePublicKey):\n        if isinstance(public_key.curve, ec.SECP256R1):\n            key_details = PublicKeyDetails.PKIX_ECDSA_P256_SHA_256\n        elif isinstance(public_key.curve, ec.SECP384R1):\n            key_details = PublicKeyDetails.PKIX_ECDSA_P384_SHA_384\n        elif isinstance(public_key.curve, ec.SECP521R1):\n            key_details = PublicKeyDetails.PKIX_ECDSA_P521_SHA_512\n        else:\n            raise ValueError(f\"Unsupported EC curve: {public_key.curve.name}\")\n    elif isinstance(public_key, rsa.RSAPublicKey):\n        if public_key.key_size == 2048:\n            if isinstance(params, padding.PKCS1v15):\n                key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256\n            else:\n                raise ValueError(\n                    f\"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}\"\n                )\n        elif public_key.key_size == 3072:\n            if isinstance(params, padding.PKCS1v15):\n                key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256\n            else:\n                raise ValueError(\n                    f\"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}\"\n                )\n        elif public_key.key_size == 4096:\n            if isinstance(params, padding.PKCS1v15):\n                key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256\n            else:\n                raise ValueError(\n                    f\"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}\"\n                )\n        else:\n            raise ValueError(f\"Unsupported RSA key size: {public_key.key_size}\")\n    elif isinstance(public_key, ed25519.Ed25519PublicKey):\n        key_details = PublicKeyDetails.PKIX_ED25519\n    # There is likely no need to explicitly detect PKIX_ED25519_PH, especially since the cryptography\n    # library does not yet support Ed25519ph.\n    else:\n        raise ValueError(f\"Unsupported public key type: {type(public_key)}\")\n    return key_details\n"
  },
  {
    "path": "sigstore/_internal/merkle.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nUtilities for verifying proof-of-inclusion within Rekor's Merkle Tree.\n\nThis code is based off Google's Trillian Merkle Tree implementation which Cosign uses to validate\nRekor entries.\n\nThe data format for the Merkle tree nodes is described in IETF's RFC 6962.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport struct\nimport typing\n\nfrom sigstore.errors import VerificationError\n\nif typing.TYPE_CHECKING:\n    from sigstore.models import TransparencyLogEntry\n\n\n_LEAF_HASH_PREFIX = 0\n_NODE_HASH_PREFIX = 1\n\n\ndef _decomp_inclusion_proof(index: int, size: int) -> tuple[int, int]:\n    \"\"\"\n    Breaks down inclusion proof for a leaf at the specified |index| in a tree of the specified\n    |size| into 2 components. The splitting point between them is where paths to leaves |index| and\n    |size-1| diverge.\n\n    Returns lengths of the bottom and upper proof parts correspondingly. The sum of the two\n    determines the correct length of the inclusion proof.\n    \"\"\"\n\n    inner = (index ^ (size - 1)).bit_length()\n    border = bin(index >> inner).count(\"1\")\n    return inner, border\n\n\ndef _chain_inner(seed: bytes, hashes: list[bytes], log_index: int) -> bytes:\n    \"\"\"\n    Computes a subtree hash for a node on or below the tree's right border. Assumes |proof| hashes\n    are ordered from lower levels to upper, and |seed| is the initial subtree/leaf hash on the path\n    located at the specified |index| on its level.\n    \"\"\"\n\n    for i in range(len(hashes)):\n        h = hashes[i]\n        if (log_index >> i) & 1 == 0:\n            seed = _hash_children(seed, h)\n        else:\n            seed = _hash_children(h, seed)\n    return seed\n\n\ndef _chain_border_right(seed: bytes, hashes: list[bytes]) -> bytes:\n    \"\"\"\n    Chains proof hashes along tree borders. This differs from inner chaining because |proof|\n    contains only left-side subtree hashes.\n    \"\"\"\n\n    for h in hashes:\n        seed = _hash_children(h, seed)\n    return seed\n\n\ndef _hash_children(lhs: bytes, rhs: bytes) -> bytes:\n    pattern = f\"B{len(lhs)}s{len(rhs)}s\"\n    data = struct.pack(pattern, _NODE_HASH_PREFIX, lhs, rhs)\n    return hashlib.sha256(data).digest()\n\n\ndef _hash_leaf(leaf: bytes) -> bytes:\n    pattern = f\"B{len(leaf)}s\"\n    data = struct.pack(pattern, _LEAF_HASH_PREFIX, leaf)\n    return hashlib.sha256(data).digest()\n\n\ndef verify_merkle_inclusion(entry: TransparencyLogEntry) -> None:\n    \"\"\"Verify the Merkle Inclusion Proof for a given Rekor entry.\"\"\"\n    inclusion_proof = entry._inner.inclusion_proof\n\n    # Figure out which subset of hashes corresponds to the inner and border nodes.\n    inner, border = _decomp_inclusion_proof(\n        inclusion_proof.log_index, inclusion_proof.tree_size\n    )\n\n    # Check against the number of hashes.\n    if len(inclusion_proof.hashes) != (inner + border):\n        raise VerificationError(\n            f\"inclusion proof has wrong size: expected {inner + border}, got \"\n            f\"{len(inclusion_proof.hashes)}\"\n        )\n\n    # The new entry's hash isn't included in the inclusion proof so we should calculate this\n    # ourselves.\n    leaf_hash: bytes = _hash_leaf(entry._inner.canonicalized_body)\n\n    # Now chain the hashes belonging to the inner and border portions. We should expect the\n    # calculated hash to match the root hash.\n    intermediate_result: bytes = _chain_inner(\n        leaf_hash, inclusion_proof.hashes[:inner], inclusion_proof.log_index\n    )\n\n    calc_hash = _chain_border_right(intermediate_result, inclusion_proof.hashes[inner:])\n\n    if calc_hash != inclusion_proof.root_hash:\n        raise VerificationError(\n            f\"inclusion proof contains invalid root hash: expected {inclusion_proof}, calculated \"\n            f\"{calc_hash.hex()}\"\n        )\n"
  },
  {
    "path": "sigstore/_internal/oidc/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nInternal OIDC and OAuth functionality for sigstore-python.\n\"\"\"\n"
  },
  {
    "path": "sigstore/_internal/oidc/oauth.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nOAuth2 flow functionality for `sigstore-python`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport http.server\nimport logging\nimport os\nimport threading\nimport urllib.parse\nimport uuid\nfrom types import TracebackType\nfrom typing import Any, cast\n\nfrom id import IdentityError\n\nfrom sigstore._utils import B64Str\nfrom sigstore.oidc import Issuer\n\n_logger = logging.getLogger(__name__)\n\n\n# This HTML is copied from the Go Sigstore library and was originally authored by Julien Vermette:\n#   https://github.com/sigstore/sigstore/blob/main/pkg/oauth/interactive.go\nAUTH_SUCCESS_HTML = \"\"\"\n<html>\n  <head>\n    <title>Sigstore Authentication</title>\n    <link id=\"favicon\" rel=\"icon\" type=\"image/svg\"/>\n    <style>\n      :root { font-family: \"Trebuchet MS\", sans-serif; height: 100%; color: #444444; overflow: hidden; }\n      body { display: flex; justify-content: center; height: 100%; margin: 0 10%; background: #FFEAD7; }\n      .container { display: flex; flex-direction: column; justify-content: space-between; }\n      .sigstore { color: #2F2E71; font-weight: bold; }\n      .header { position: absolute; top: 30px; left: 22px; }\n      .title { font-size: 3.5em; margin-bottom: 30px; animation: 750ms ease-in-out 0s 1 show; }\n      .content { font-size: 1.5em; animation: 250ms hide, 750ms ease-in-out 250ms 1 show; }\n      .anchor { position: relative; }\n      .links { display: flex; justify-content: space-between; font-size: 1.2em; padding: 60px 0; position: absolute; bottom: 0; left: 0; right: 0; animation: 500ms hide, 750ms ease-in-out 500ms 1 show; }\n      .link { color: #444444; text-decoration: none; user-select: none; }\n      .link:hover { color: #6349FF; }\n      .link:hover>.arrow { transform: scaleX(1.5) translateX(3px); }\n      .link:hover>.sigstore { color: inherit; }\n      .link, .arrow { transition: 200ms; }\n      .arrow { display: inline-block; margin-left: 6px; transform: scaleX(1.5); }\n      @keyframes hide { 0%, 100% { opacity: 0; } }\n      @keyframes show { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; } }\n    </style>\n  </head>\n  <body>\n    <div class=\"container\">\n      <div>\n        <a class=\"header\" href=\"https://sigstore.dev\">\n          <svg id=\"logo\" xmlns=\"http://www.w3.org/2000/svg\" xml:space=\"preserve\" width=\"28.14\" height=\"30.3\">\n            <circle r=\"7\" cx=\"14\" cy=\"15\" fill=\"#FFEAD7\"></circle>\n            <path fill=\"#2F2E71\" d=\"M27.8 10.9c-.3-1.2-.9-2.2-1.7-3.1-.6-.7-1.3-1.3-2-2-.7-.6-1.2-1.3-1.5-2.1-.2-.4-.4-.8-.7-1.2-.5-.7-1.3-1.2-2.1-1.6-1.3-.7-2.7-.9-4.2-.9-.8 0-1.6.1-2.4.3-1.2.2-2.3.7-3.4 1.3-.7.4-1.3.9-1.9 1.4-1 .8-2 1.6-2.8 2.6-.6.8-1.4 1.3-2.2 1.8-.8.4-1.4 1-2 1.6-.6.6-.9 1.3-.9 2.1 0 .6.1 1.2.2 1.7.2.9.6 1.7.9 2.6.2.5.3 1 .3 1.5s0 1-.1 1.5c-.1 1.1 0 2.3.2 3.4.2 1 .8 1.8 1.8 2.2.1.1.3.1.4.1.2.1.2.2.1.3l-.1.1c-.4.5-.7 1.1-.6 1.8.1 1.1 1.3 1.8 2.3 1.3.6-.2 1.2 0 1.4.4.1.1.1.2.2.3.2.5.4.9.7 1.3.4.5.9.7 1.6.6.4-.1.8-.2 1.2-.4.7-.4 1.3-.9 2-1.5.2-.2.4-.2.7-.2.4 0 .8.2 1.2.5.6.4 1.2.7 1.9.9 1.3.4 2.5.5 3.8.2 1.3-.3 2.4-.9 3.4-1.6.7-.5 1.2-1 1.6-1.7.4-.7.6-1.4.8-2.2.3-1.1.4-2.2.4-3.4.1-1 .2-1.9.5-2.8.2-.7.5-1.4.8-2.1.2-.6.4-1.2.5-1.9.1-1.1 0-2.1-.3-3.1zM14.9.8c.3-.1.7-.1 1-.1h.3c1.1 0 2.1.2 3.1.5.6.2 1.2.6 1.7 1s.7.9.9 1.4v.1c0 .1 0 .2-.1.2s-.1 0-.2-.1c-.4-.4-.7-.8-1.1-1.1-.6-.5-1.2-.9-2-1.1-1.1-.3-2.1-.5-3.2-.7h-.6c.1 0 .1 0 .2-.1-.1 0 0 0 0 0zm-4.5 12.4c.6 0 1.1.5 1.2 1.2 0 .6-.5 1.2-1.2 1.2-.6 0-1.2-.5-1.1-1.2 0-.7.5-1.2 1.1-1.2zm3.8 1.3v-3.4c0-2.3 2-3.1 3.6-2.5.3.1.6.3.9.5.2.2.2.5.1.8-.2.2-.4.3-.7.1-.2-.1-.5-.2-.7-.3-.6-.2-1.3 0-1.6.4-.1.2-.2.4-.2.7-.1.5 0 .9 0 1.4v5.9c0 1.2-.6 2.1-1.8 2.4-1 .3-1.9.2-2.7-.6-.2-.2-.3-.5-.1-.7.1-.2.4-.3.7-.2.3.1.6.3.9.4 1 .1 1.7-.3 1.7-1.4-.1-1.2-.1-2.3-.1-3.5zm-8.8 7.6h-.1c-.1-.1-.2-.1-.3-.2-.2-.2-.4-.3-.6-.5-.3-.3-.5-.6-.7-1-.4-.8-.8-1.7-1-2.7-.1-.5-.2-1-.2-1.5s-.1-1-.2-1.4c-.1-.7-.2-1.5-.2-2.2 0-.9.1-1.7.4-2.5.3-.9.7-1.7 1.4-2.4.6-.6 1.1-1.2 1.7-1.8.1-.1.3-.2.4-.2 0 .1-.1.3-.2.4-.3.4-.6.7-.9 1.1-.5.6-.9 1.2-1.2 1.8-.4.7-.7 1.4-.9 2.2-.1.4-.2.8-.2 1.2 0 .4-.1.8 0 1.3 0 .6.1 1.1.2 1.6.1.6.2 1.1.2 1.7 0 .7.2 1.4.4 2.1 0 .2.2.3.2.5.3.6.6 1.1 1.1 1.5.2.2.4.5.6.7 0 0 0 .1.1.1v.2zM8 24.6c-.4 0-.7.1-1.1.2-.4.1-.6-.1-.7-.5 0-.1-.1-.3 0-.4.1-.3.3-.3.5-.1.2.2.5.4.7.5.1.1.2.1.4.1.1 0 .2.1.4.2H8zm7.6 2.1c-.3.2-.7.3-1.1.3-.3 0-.6-.1-.9-.1h-.2c-.4.1-.7.1-1.1.2-.1 0-.3 0-.4.1H11c-.4 0-.7-.2-1-.5-.1-.1-.2-.3-.3-.5-.1-.1-.1-.2-.1-.4 0-.1.1-.1.2-.1h.1c.5.3 1.1.4 1.6.5.7.1 1.4.2 2.1.2.4 0 .7.1 1.1.1h.8c.2.1.1.1.1.2zm3.7-2.5c-.7.4-1.5.7-2.3.9-.2 0-.5.1-.7.1-.2 0-.5 0-.7.1-.4.1-.8 0-1.2 0-.3 0-.6-.1-.9 0h-.2c-.4-.1-.9-.2-1.3-.3-.5-.1-1-.3-1.4-.5-.4-.1-.8-.3-1.1-.5-.2-.1-.4-.3-.6-.4-.6-.6-1.2-1.1-1.7-1.6-.4-.5-.8-.9-1.2-1.4-.4-.6-.7-1.2-1-1.9l-.3-.9c-.1-.3-.2-.5-.2-.8v-.8c.3.8.5 1.7.9 2.5.7 1.6 1.7 3 3 4.1 1.4 1.1 2.9 1.8 4.6 2.1.9.2 1.8.2 2.7.2 1.1-.1 2.2-.3 3.2-.8.2-.1.3-.2.5-.2 0 .1 0 .1-.1.1zm.1-8.7c-.6 0-1.1-.5-1.1-1.2 0-.6.5-1.2 1.2-1.2.6 0 1.1.5 1.1 1.2s-.5 1.3-1.2 1.2zm6.2 5.7c0 .4-.1.8-.2 1.2-.1.4-.1.9-.3 1.3-.1.4-.2.7-.4 1.1-.1.3-.3.6-.6.8-.3.2-.5.4-.9.5-.4.2-.7.3-1.2.3h-.9c-.2-.1-.2-.1-.1-.3.1-.2.3-.3.5-.4.3-.2.6-.5.8-.7.7-.7 1.3-1.6 1.9-2.4.4-.4.6-1 .9-1.5.1-.1.1-.2.2-.3.3.2.3.3.3.4zm-15-16.8c1.7-.8 3.5-1.1 5.3-.9.4 0 .8.1 1.1.3l1.8.6c.6.2 1.2.5 1.7.8.7.4 1.3.9 1.9 1.5.8.8 1.5 1.6 2 2.6.3.6.5 1.2.7 1.8.2.7.4 1.5.4 2.2v.9c0 .4-.1.8-.1 1.2v-1c0-1.2-.3-2.3-.6-3.4l-.6-1.5c-.2-.6-.5-1.1-.9-1.6-.1-.1-.3-.1-.4-.2-.1 0-.1 0-.2-.1-.5-.5-1.1-1-1.7-1.5-.8-.6-1.7-1.1-2.6-1.4-.4-.2-.8-.3-1.2-.4-.9-.2-1.8-.4-2.7-.3h-.9c-.3 0-.6.1-1 .2-.6.1-1.2.3-1.7.5h-.1s-.1 0 0-.1c0 0 0-.1-.1-.2m16.2 11.1c-.1-.8 0-1.7 0-2.5-.1-.8-.2-1.6-.4-2.4.5.7.6 1.6.7 2.4 0 .8 0 1.7-.3 2.5zm.6.5c0-.3.1-.7.2-1.1.1-.4.1-.9.1-1.3v-.4c0-.8-.2-1.6-.4-2.4-.4-.9-.8-1.6-1.4-2.4-.5-.6-.9-1.2-1.4-1.8l-.2-.2c.1 0 .1 0 .1.1 1 .8 1.8 1.6 2.4 2.7.5 1 .9 2 1 3.1.3 1.3.1 2.5-.4 3.7z\"/>\n          </svg><svg xmlns=\"http://www.w3.org/2000/svg\" xml:space=\"preserve\" width=\"120\" height=\"30.3\" viewBox=\"28.14 0 120 30.3\">\n            <path fill=\"#2F2E71\" d=\"M57.7 18c.9 0 1.9-.1 2.9.3.9.3 1.5.9 1.7 2 .1 1-.2 1.9-1.1 2.5-1.1.8-2.3.9-3.6 1-1.4 0-2.9 0-4.3-.6-1.6-.7-1.8-2.6-.4-3.6.3-.2.2-.3.1-.5-.7-.8-.7-2.2.3-2.8.3-.2.2-.3 0-.6-1.4-1.6-.7-4.1 1.3-4.8 1.6-.6 3.2-.6 4.8-.1.2.1.3.1.5-.1.3-.3.6-.5.9-.7.5-.3 1.1-.3 1.4 0 .3.4.3.9-.2 1.3-.7.5-.8 1-.6 1.9.4 1.5-.6 2.9-2.1 3.4-1.3.5-2.7.5-4 .2-.3-.1-.5-.1-.6.2-.1.3-.1.6.2.8.2.1.5.2.8.2h2zm-.6-2.7c.3 0 .5 0 .7-.1.8-.2 1.3-.7 1.4-1.4.1-.7-.3-1.3-.9-1.6-.8-.3-1.6-.3-2.4 0-1 .4-1.2 1.7-.6 2.4.5.6 1.2.7 1.8.7zm-.2 4.6h-1.8c-.4 0-.6.2-.7.5-.3.7.1 1.2.9 1.4 1.2.2 2.5.2 3.7-.1l.6-.3c.5-.3.4-1.1-.1-1.3-.2-.1-.5-.2-.7-.2h-1.9zm58.6-3.3h-3.2c-.3 0-.3.1-.3.4.2 1.2 1.3 2.1 2.8 2.3 1.1.1 2.1-.1 2.9-.9.2-.2.4-.3.6-.4.4-.2.8-.2 1.1.1.4.3.4.7.3 1.1-.3.9-.9 1.4-1.7 1.8-2.3 1-4.5.9-6.5-.6-1-.7-1.5-1.8-1.7-3-.3-1.7-.2-3.3.8-4.7 1.3-1.8 3.1-2.4 5.2-2.1 2 .3 3.4 1.3 4 3.2.2.6.3 1.2.2 1.8-.1.8-.4 1.1-1.2 1.1-1.1-.1-2.2-.1-3.3-.1zm-.7-1.9h2.4c.4 0 .4-.1.4-.5-.3-1.1-1.2-1.9-2.5-2-1.5-.1-2.6.6-3.1 1.9-.2.5-.1.6.4.6h2.4zm-23 6.7c-3.3 0-5.5-2.2-5.6-5.5 0-3.2 2.2-5.6 5.4-5.6 3.4 0 5.7 2.2 5.8 5.5 0 3.4-2.2 5.6-5.6 5.6zm0-2.1c1.9 0 3.2-1.3 3.3-3.3.1-2-1.3-3.4-3.2-3.4-1.8 0-3.2 1.4-3.2 3.3-.1 1.9 1.2 3.4 3.1 3.4zm-22.6 2.1c-1.1 0-2.4-.3-3.5-1.4-.3-.4-.6-.8-.7-1.3 0-.4.1-.7.4-.9.3-.2.7-.2 1 0 .3.2.6.4.8.6.9.9 2 1.1 3.2.9.6-.1 1-.5 1-1 .1-.5-.2-1-.8-1.2-.7-.3-1.4-.3-2.1-.5-.8-.2-1.6-.4-2.2-.9-1.5-1.2-1.4-3.5.2-4.6 1.2-.8 2.6-.9 4-.7 1 .1 1.9.5 2.6 1.2.3.3.4.6.5.9.1.4 0 .7-.3 1-.3.3-.7.3-1 .1-.4-.2-.7-.5-1.1-.8-.8-.6-1.7-.8-2.7-.5-.6.1-.9.5-.9 1s.3.9.8 1.1c.9.3 1.9.4 2.9.7.6.2 1.1.4 1.5.8 1.5 1.4 1.1 4.1-.9 5.1-.7.3-1.5.4-2.7.4zm-30.3 0c-1.5 0-3-.3-4.1-1.5-.3-.3-.4-.6-.5-1-.1-.4-.1-.8.3-1.1.4-.2.9-.2 1.3.1.3.2.6.5.8.7.9.8 1.9.9 3 .7.6-.1.9-.5.9-1.1 0-.5-.3-1-.8-1.2l-2.7-.6c-1.1-.3-2-.8-2.4-2-.6-1.7.4-3.4 2.2-3.9 1.7-.5 3.3-.3 4.8.5.6.3 1 .8 1.2 1.4.1.4.1.9-.3 1.1-.4.3-.8.2-1.2-.1-.4-.3-.7-.7-1.2-.9-.8-.4-1.5-.5-2.4-.3-.5.1-.8.4-.8.9s.2.8.7 1c.7.3 1.4.3 2.1.5.8.2 1.5.3 2.2.8 1.2.8 1.5 2.5.9 3.8-.7 1.4-1.9 1.8-3.3 2-.3.2-.5.2-.7.2zM78 15.8v-2.6c0-.3-.1-.4-.4-.4h-1.3c-.6-.1-.9-.5-.9-1s.4-1 .9-1h1.3c.3 0 .4-.1.4-.4V8.9c0-.7.5-1.1 1.1-1.2.6 0 1.1.4 1.2 1v.6c0 .4-.2 1 .1 1.3.3.3.9.1 1.3.1h1.6c.4 0 .7.3.8.8.1.4-.1.8-.4 1.1-.3.2-.6.2-.9.2h-2.1c-.3 0-.4 0-.4.4v4.7c0 1.1.9 1.6 1.9 1.1.3-.1.5-.3.7-.5.4-.3.8-.3 1.2 0 .4.3.4.8.2 1.2-.3.7-.9 1.1-1.5 1.3-1.3.5-2.6.5-3.8-.4-.7-.6-1-1.4-1.1-2.4.1-.7.1-1.6.1-2.4zm24.7-4.1c.8-1 1.8-1.4 3-1.3.7.1 1.4.3 1.9.9.3.4.5.9.4 1.5-.1.4-.3.7-.7.9-.5.2-.9 0-1.3-.3-.7-.7-1.3-.9-2.1-.5-.5.3-.8.7-1 1.3-.2.5-.3 1.1-.3 1.6v4.5c0 .5-.2.9-.6 1.1-.4.2-.8.2-1.2-.1-.4-.3-.5-.7-.5-1.1v-8.4c0-.7.4-1.2 1-1.3.7-.1 1.1.3 1.3 1.1.1-.1.1 0 .1.1zm-54 4.2v4.3c0 .8-.6 1.3-1.4 1.2-.5-.1-.9-.5-.9-1.1v-8.7c0-.7.5-1.1 1.2-1.1s1.1.4 1.2 1.2c-.1 1.3-.1 2.8-.1 4.2zm.3-8.2c0 .8-.6 1.4-1.4 1.4-.8 0-1.5-.6-1.5-1.4 0-.8.7-1.4 1.5-1.4s1.4.6 1.4 1.4z\"/>\n          </svg>\n        </a>\n      </div>\n      <div>\n        <div class=\"title\">\n          <span class=\"sigstore\">sigstore </span>\n          <span>authentication successful!</span>\n        </div>\n        <div class=\"content\">\n          <span>You may now close this page.</span>\n        </div>\n      </div>\n      <div class=\"anchor\">\n        <div class=\"links\">\n          <a href=\"https://sigstore.dev/\" class=\"link login\"><span class=\"sigstore\">sigstore</span> home <span class=\"arrow\">→</span></a>\n          <a href=\"https://docs.sigstore.dev/\" class=\"link login\"><span class=\"sigstore\">sigstore</span> documentation <span class=\"arrow\">→</span></a>\n          <a href=\"https://blog.sigstore.dev/\" class=\"link\"><span class=\"sigstore\">sigstore</span> blog <span class=\"arrow\">→</span></a>\n        </div>\n      </div>\n    </div>\n    <script>\n      document.getElementById(\"favicon\").setAttribute(\"href\", \"data:image/svg+xml,\" + encodeURIComponent(document.getElementById(\"logo\").outerHTML));\n    </script>\n  </body>\n</html>\n\"\"\"\n\n\nclass _OAuthFlow:\n    def __init__(self, client_id: str, client_secret: str, issuer: Issuer):\n        self._client_id = client_id\n        self._client_secret = client_secret\n        self._issuer = issuer\n        self._server = _OAuthRedirectServer(\n            self._client_id, self._client_secret, self._issuer\n        )\n        self._server_thread = threading.Thread(\n            target=lambda server: server.serve_forever(),\n            args=(self._server,),\n        )\n\n    def __enter__(self) -> _OAuthRedirectServer:\n        self._server_thread.start()\n\n        return self._server\n\n    def __exit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        self._server.shutdown()\n        self._server_thread.join()\n\n\nclass _OAuthRedirectHandler(http.server.BaseHTTPRequestHandler):\n    # Short socket timeout to prevent blocking serve_forever() on idle connections\n    timeout = 1\n\n    def log_message(self, format: str, *_args: Any) -> None:\n        pass\n\n    def do_GET(self) -> None:\n        _logger.debug(f\"GET: {self.path} with {dict(self.headers)}\")\n        server = cast(_OAuthRedirectServer, self.server)\n\n        # The redirect server only needs one request per connection.\n        # Close conn immediately to prevent keep-alive from blocking.\n        self.close_connection = True\n\n        # If the auth response has already been populated, the main thread will be stopping this\n        # thread and accessing the auth response shortly so we should stop servicing any requests.\n        if server.auth_response is not None:\n            _logger.debug(f\"{self.path} unavailable (teardown)\")\n            self.send_response(404)\n            self.end_headers()\n            return None\n\n        r = urllib.parse.urlsplit(self.path)\n\n        # We only understand two kinds of requests:\n        # 1. The response from a successful OAuth redirect\n        # 2. The initial request to /, which kicks off (1)\n        if r.path == server.redirect_path:\n            self.send_response(200)\n            self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n            body = AUTH_SUCCESS_HTML.encode(\"utf-8\")\n            self.send_header(\"Content-Length\", str(len(body)))\n            self.end_headers()\n            self.wfile.write(body)\n            server.auth_response = urllib.parse.parse_qs(r.query)\n        elif r.path == server.auth_request_path:\n            self.send_response(302)\n            self.send_header(\"Location\", server.auth_endpoint)\n            self.end_headers()\n        else:\n            # Anything else sends a \"Not Found\" response.\n            self.send_response(404)\n            self.end_headers()\n\n\nOOB_REDIRECT_URI = \"urn:ietf:wg:oauth:2.0:oob\"\n\n\nclass _OAuthSession:\n    def __init__(self, client_id: str, client_secret: str, issuer: Issuer):\n        self.__poison = False\n\n        self._client_id = client_id\n        self._client_secret = client_secret\n        self._issuer = issuer\n        self._state = str(uuid.uuid4())\n\n        self.code_verifier = B64Str(\n            base64.urlsafe_b64encode(os.urandom(32)).rstrip(b\"=\").decode()\n        )\n\n    @property\n    def state(self) -> str:\n        return self._state\n\n    @property\n    def code_challenge(self) -> str:\n        return B64Str(\n            base64.urlsafe_b64encode(\n                hashlib.sha256(self.code_verifier.encode()).digest()\n            )\n            .rstrip(b\"=\")\n            .decode()\n        )\n\n    def auth_endpoint(self, redirect_uri: str) -> str:\n        # Defensive programming: we don't have a nice way to limit the\n        # lifetime of the OAuth session here, so we use the internal\n        # \"poison\" flag to check if we're attempting to reuse it in a way\n        # that would compromise the flow's security (i.e. state reuse).\n        if self.__poison:\n            raise IdentityError(\"internal error: OAuth endpoint misuse\")\n        else:\n            self.__poison = True\n\n        params = self._auth_params(redirect_uri)\n        return f\"{self._issuer.oidc_config.authorization_endpoint}?{urllib.parse.urlencode(params)}\"\n\n    def _auth_params(self, redirect_uri: str) -> dict[str, Any]:\n        return {\n            \"response_type\": \"code\",\n            \"client_id\": self._client_id,\n            \"client_secret\": self._client_secret,\n            \"scope\": \"openid email\",\n            \"redirect_uri\": redirect_uri,\n            \"code_challenge\": self.code_challenge,\n            \"code_challenge_method\": \"S256\",\n            \"state\": self._state,\n        }\n\n\nclass _OAuthRedirectServer(http.server.HTTPServer):\n    def __init__(self, client_id: str, client_secret: str, issuer: Issuer) -> None:\n        super().__init__((\"localhost\", 0), _OAuthRedirectHandler)\n        self.oauth_session = _OAuthSession(client_id, client_secret, issuer)\n        self.auth_response: dict[str, list[str]] | None = None\n        self._is_out_of_band = False\n\n    @property\n    def base_uri(self) -> str:\n        # NOTE: We'd ideally use `self.server_name` here, but it uses\n        # the FQDN internally (which in turn confuses Sigstore).\n        return f\"http://localhost:{self.server_port}\"\n\n    @property\n    def auth_request_path(self) -> str:\n        # TODO: Maybe this should be /auth, for clarity?\n        return \"/\"\n\n    @property\n    def redirect_path(self) -> str:\n        return \"/auth/callback\"\n\n    @property\n    def redirect_uri(self) -> str:\n        return (\n            (self.base_uri + self.redirect_path)\n            if not self._is_out_of_band\n            else OOB_REDIRECT_URI\n        )\n\n    @property\n    def auth_endpoint(self) -> str:\n        return self.oauth_session.auth_endpoint(self.redirect_uri)\n\n    def enable_oob(self) -> None:\n        _logger.debug(\"enabling out-of-band OAuth flow\")\n        self._is_out_of_band = True\n\n    def is_oob(self) -> bool:\n        return self._is_out_of_band\n"
  },
  {
    "path": "sigstore/_internal/rekor/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nAPIs for interacting with Rekor.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport typing\nfrom abc import ABC, abstractmethod\n\nimport rekor_types\nimport requests\nfrom cryptography.x509 import Certificate\n\nfrom sigstore._utils import base64_encode_pem_cert\nfrom sigstore.dsse import Envelope\nfrom sigstore.hashes import Hashed\n\nif typing.TYPE_CHECKING:\n    from sigstore.models import TransparencyLogEntry\n\n__all__ = [\n    \"_hashedrekord_from_parts\",\n]\n\nEntryRequestBody = typing.NewType(\"EntryRequestBody\", dict[str, typing.Any])\n\n\nclass RekorClientError(Exception):\n    \"\"\"\n    A generic error in the Rekor client.\n    \"\"\"\n\n    def __init__(self, http_error: requests.HTTPError):\n        \"\"\"\n        Create a new `RekorClientError` from the given `requests.HTTPError`.\n        \"\"\"\n        if http_error.response is not None:\n            try:\n                error = rekor_types.Error.model_validate_json(http_error.response.text)\n                super().__init__(f\"{error.code}: {error.message}\")\n            except Exception:\n                super().__init__(\n                    f\"Rekor returned an unknown error with HTTP {http_error.response.status_code}\"\n                )\n        else:\n            super().__init__(f\"Unexpected Rekor error: {http_error}\")\n\n\nclass RekorLogSubmitter(ABC):\n    \"\"\"\n    Abstract class to represent a Rekor log entry submitter.\n\n    Intended to be implemented by RekorClient and RekorV2Client.\n    \"\"\"\n\n    @abstractmethod\n    def create_entry(\n        self,\n        request: EntryRequestBody,\n    ) -> TransparencyLogEntry:\n        \"\"\"\n        Submit the request to Rekor.\n        \"\"\"\n        pass\n\n    @classmethod\n    @abstractmethod\n    def _build_hashed_rekord_request(\n        self, hashed_input: Hashed, signature: bytes, certificate: Certificate\n    ) -> EntryRequestBody:\n        \"\"\"\n        Construct a hashed rekord request to submit to Rekor.\n        \"\"\"\n        pass\n\n    @classmethod\n    @abstractmethod\n    def _build_dsse_request(\n        self, envelope: Envelope, certificate: Certificate\n    ) -> EntryRequestBody:\n        \"\"\"\n        Construct a dsse request to submit to Rekor.\n        \"\"\"\n        pass\n\n\n# TODO: This should probably live somewhere better.\ndef _hashedrekord_from_parts(\n    cert: Certificate, sig: bytes, hashed: Hashed\n) -> rekor_types.Hashedrekord:\n    return rekor_types.Hashedrekord(\n        spec=rekor_types.hashedrekord.HashedrekordV001Schema(\n            signature=rekor_types.hashedrekord.Signature(\n                content=base64.b64encode(sig).decode(),\n                public_key=rekor_types.hashedrekord.PublicKey(\n                    content=base64_encode_pem_cert(cert),\n                ),\n            ),\n            data=rekor_types.hashedrekord.Data(\n                hash=rekor_types.hashedrekord.Hash(\n                    algorithm=hashed._as_hashedrekord_algorithm(),\n                    value=hashed.digest.hex(),\n                )\n            ),\n        )\n    )\n"
  },
  {
    "path": "sigstore/_internal/rekor/checkpoint.py",
    "content": "# Copyright 2023 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nRekor Checkpoint machinery.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport re\nimport struct\nimport typing\nfrom dataclasses import dataclass\n\nfrom pydantic import BaseModel, Field, StrictStr\n\nfrom sigstore._utils import KeyID\nfrom sigstore.errors import VerificationError\n\nif typing.TYPE_CHECKING:\n    from sigstore._internal.trust import RekorKeyring\n    from sigstore.models import TransparencyLogEntry\n\n\n@dataclass(frozen=True)\nclass RekorSignature:\n    \"\"\"\n    Represents a `RekorSignature` containing:\n\n    - the name of the signature, e.g. \"rekor.sigstage.dev\"\n    - the signature hash\n    - the base64 signature\n    \"\"\"\n\n    name: str\n    sig_hash: bytes\n    signature: bytes\n\n\nclass LogCheckpoint(BaseModel):\n    \"\"\"\n    Represents a Rekor `LogCheckpoint` containing:\n\n    - an origin, e.g. \"rekor.sigstage.dev - 8050909264565447525\"\n    - the size of the log,\n    - the hash of the log,\n    - and any optional ancillary constants, e.g. \"Timestamp: 1679349379012118479\"\n\n    See: <https://github.com/transparency-dev/formats/blob/main/log/README.md>\n    \"\"\"\n\n    origin: StrictStr\n    log_size: int\n    log_hash: StrictStr\n    other_content: list[str]\n\n    @classmethod\n    def from_text(cls, text: str) -> LogCheckpoint:\n        \"\"\"\n        Serialize from the text header (\"note\") of a SignedNote.\n        \"\"\"\n\n        lines = text.strip().split(\"\\n\")\n        if len(lines) < 3:\n            raise VerificationError(\"malformed LogCheckpoint: too few items in header\")\n\n        origin = lines[0]\n        if len(origin) == 0:\n            raise VerificationError(\"malformed LogCheckpoint: empty origin\")\n\n        log_size = int(lines[1])\n        root_hash = base64.b64decode(lines[2]).hex()\n\n        return LogCheckpoint(\n            origin=origin,\n            log_size=log_size,\n            log_hash=root_hash,\n            other_content=lines[3:],\n        )\n\n    @classmethod\n    def to_text(self) -> str:\n        \"\"\"\n        Serialize a `LogCheckpoint` into text format.\n        See class definition for a prose description of the format.\n        \"\"\"\n        return \"\\n\".join(\n            [self.origin, str(self.log_size), self.log_hash, *self.other_content]\n        )\n\n\n@dataclass(frozen=True)\nclass SignedNote:\n    \"\"\"\n    Represents a \"signed note\" containing a note and its corresponding list of signatures.\n    \"\"\"\n\n    note: StrictStr = Field(..., alias=\"note\")\n    signatures: list[RekorSignature] = Field(..., alias=\"signatures\")\n\n    @classmethod\n    def from_text(cls, text: str) -> SignedNote:\n        \"\"\"\n        Deserialize from a bundled text 'note'.\n\n        A note contains:\n        - a name, a string associated with the signer,\n        - a separator blank line,\n        - and signature(s), each signature takes the form\n            `\\u2014 NAME SIGNATURE\\n`\n          (where \\u2014 == em dash).\n\n        This is derived from Rekor's `UnmarshalText`:\n        <https://github.com/sigstore/rekor/blob/4b1fa6661cc6dfbc844b4c6ed9b1f44e7c5ae1c0/pkg/util/signed_note.go#L141>\n        \"\"\"\n\n        separator: str = \"\\n\\n\"\n        if text.count(separator) != 1:\n            raise VerificationError(\n                \"note must contain one blank line, delineating the text from the signature block\"\n            )\n        split = text.index(separator)\n\n        header: str = text[: split + 1]\n        data: str = text[split + len(separator) :]\n\n        if len(data) == 0:\n            raise VerificationError(\n                \"malformed Note: must contain at least one signature\"\n            )\n        if data[-1] != \"\\n\":\n            raise VerificationError(\n                \"malformed Note: data section must end with newline\"\n            )\n\n        sig_parser = re.compile(r\"\\u2014 (\\S+) (\\S+)\\n\")\n        signatures: list[RekorSignature] = []\n        for name, signature in re.findall(sig_parser, data):\n            signature_bytes: bytes = base64.b64decode(signature)\n            if len(signature_bytes) < 5:\n                raise VerificationError(\n                    \"malformed Note: signature contains too few bytes\"\n                )\n\n            signature = RekorSignature(\n                name=name,\n                sig_hash=struct.unpack(\">4s\", signature_bytes[0:4])[0],\n                signature=base64.b64encode(signature_bytes[4:]),\n            )\n            signatures.append(signature)\n\n        return cls(note=header, signatures=signatures)\n\n    def verify(self, rekor_keyring: RekorKeyring, key_id: KeyID) -> None:\n        \"\"\"\n        Verify the `SignedNote` using the given RekorKeyring and KeyID.\n        \"\"\"\n\n        note = str.encode(self.note)\n\n        for sig in self.signatures:\n            if sig.sig_hash == key_id[:4]:\n                try:\n                    rekor_keyring.verify(\n                        key_id=key_id,\n                        signature=base64.b64decode(sig.signature),\n                        data=note,\n                    )\n                    return\n                except VerificationError as sig_err:\n                    raise VerificationError(f\"checkpoint: invalid signature: {sig_err}\")\n\n        raise VerificationError(\n            f\"checkpoint: Signature not found for log ID {key_id.hex()}\"\n        )\n\n\n@dataclass(frozen=True)\nclass SignedCheckpoint:\n    \"\"\"\n    Represents a *signed* `Checkpoint`: a `LogCheckpoint` and its corresponding `SignedNote`.\n    \"\"\"\n\n    signed_note: SignedNote\n    checkpoint: LogCheckpoint\n\n    @classmethod\n    def from_text(cls, text: str) -> SignedCheckpoint:\n        \"\"\"\n        Create a new `SignedCheckpoint` from the text representation.\n        \"\"\"\n\n        signed_note = SignedNote.from_text(text)\n        checkpoint = LogCheckpoint.from_text(signed_note.note)\n        return cls(signed_note=signed_note, checkpoint=checkpoint)\n\n\ndef verify_checkpoint(rekor_keyring: RekorKeyring, entry: TransparencyLogEntry) -> None:\n    \"\"\"\n    Verify the inclusion proof's checkpoint.\n    \"\"\"\n\n    inclusion_proof = entry._inner.inclusion_proof\n    if inclusion_proof.checkpoint is None:\n        raise VerificationError(\"Inclusion proof does not contain a checkpoint\")\n\n    # verification occurs in two stages:\n    # 1) verify the signature on the checkpoint\n    # 2) verify the root hash in the checkpoint matches the root hash from the inclusion proof.\n    signed_checkpoint = SignedCheckpoint.from_text(inclusion_proof.checkpoint.envelope)\n    signed_checkpoint.signed_note.verify(\n        rekor_keyring,\n        KeyID(entry._inner.log_id.key_id),\n    )\n\n    checkpoint_hash = signed_checkpoint.checkpoint.log_hash\n    root_hash = inclusion_proof.root_hash.hex()\n\n    if checkpoint_hash != root_hash:\n        raise VerificationError(\n            \"Inclusion proof contains invalid root hash signature: \",\n            f\"expected {checkpoint_hash} got {root_hash}\",\n        )\n"
  },
  {
    "path": "sigstore/_internal/rekor/client.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nClient implementation for interacting with Rekor (v1).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport json\nimport logging\nimport threading\nfrom abc import ABC\nfrom dataclasses import dataclass\nfrom typing import Any\n\nimport rekor_types\nimport requests\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.x509 import Certificate\n\nfrom sigstore._internal import USER_AGENT\nfrom sigstore._internal.rekor import (\n    EntryRequestBody,\n    RekorClientError,\n    RekorLogSubmitter,\n)\nfrom sigstore.dsse import Envelope\nfrom sigstore.hashes import Hashed\nfrom sigstore.models import TransparencyLogEntry\n\n_logger = logging.getLogger(__name__)\n\nDEFAULT_REKOR_URL = \"https://rekor.sigstore.dev\"\nSTAGING_REKOR_URL = \"https://rekor.sigstage.dev\"\n\n\n@dataclass(frozen=True)\nclass RekorLogInfo:\n    \"\"\"\n    Represents information about the Rekor log.\n    \"\"\"\n\n    root_hash: str\n    tree_size: int\n    signed_tree_head: str\n    tree_id: str\n    raw_data: dict[str, Any]\n\n    @classmethod\n    def from_response(cls, dict_: dict[str, Any]) -> RekorLogInfo:\n        \"\"\"\n        Create a new `RekorLogInfo` from the given API response.\n        \"\"\"\n        return cls(\n            root_hash=dict_[\"rootHash\"],\n            tree_size=dict_[\"treeSize\"],\n            signed_tree_head=dict_[\"signedTreeHead\"],\n            tree_id=dict_[\"treeID\"],\n            raw_data=dict_,\n        )\n\n\nclass _Endpoint(ABC):\n    def __init__(self, url: str, session: requests.Session) -> None:\n        # Note that _Endpoint may not be thread safe if the same Session is provided\n        # to an _Endpoint in multiple threads\n        self.url = url\n        self.session = session\n\n\nclass RekorLog(_Endpoint):\n    \"\"\"\n    Represents a Rekor instance's log endpoint.\n    \"\"\"\n\n    def get(self) -> RekorLogInfo:\n        \"\"\"\n        Returns information about the Rekor instance's log.\n        \"\"\"\n        resp: requests.Response = self.session.get(self.url)\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            raise RekorClientError(http_error)\n        return RekorLogInfo.from_response(resp.json())\n\n    @property\n    def entries(self) -> RekorEntries:\n        \"\"\"\n        Returns a `RekorEntries` capable of accessing detailed information\n        about individual log entries.\n        \"\"\"\n        return RekorEntries(f\"{self.url}/entries\", session=self.session)\n\n\nclass RekorEntries(_Endpoint):\n    \"\"\"\n    Represents the individual log entry endpoints on a Rekor instance.\n    \"\"\"\n\n    def get(\n        self, *, uuid: str | None = None, log_index: int | None = None\n    ) -> TransparencyLogEntry:\n        \"\"\"\n        Retrieve a specific log entry, either by UUID or by log index.\n\n        Either `uuid` or `log_index` must be present, but not both.\n        \"\"\"\n        if not (bool(uuid) ^ bool(log_index)):\n            raise ValueError(\"uuid or log_index required, but not both\")\n\n        resp: requests.Response\n\n        if uuid is not None:\n            resp = self.session.get(f\"{self.url}/{uuid}\")\n        else:\n            resp = self.session.get(self.url, params={\"logIndex\": log_index})\n\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            raise RekorClientError(http_error)\n        return TransparencyLogEntry._from_v1_response(resp.json())\n\n    def post(\n        self,\n        payload: EntryRequestBody,\n    ) -> TransparencyLogEntry:\n        \"\"\"\n        Submit a new entry for inclusion in the Rekor log.\n        \"\"\"\n\n        _logger.debug(f\"proposed: {json.dumps(payload)}\")\n\n        resp: requests.Response = self.session.post(self.url, json=payload)\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            raise RekorClientError(http_error)\n\n        integrated_entry = resp.json()\n        _logger.debug(f\"integrated: {integrated_entry}\")\n        return TransparencyLogEntry._from_v1_response(integrated_entry)\n\n    @property\n    def retrieve(self) -> RekorEntriesRetrieve:\n        \"\"\"\n        Returns a `RekorEntriesRetrieve` capable of retrieving entries.\n        \"\"\"\n        return RekorEntriesRetrieve(f\"{self.url}/retrieve/\", session=self.session)\n\n\nclass RekorEntriesRetrieve(_Endpoint):\n    \"\"\"\n    Represents the entry retrieval endpoints on a Rekor instance.\n    \"\"\"\n\n    def post(\n        self,\n        expected_entry: rekor_types.Hashedrekord | rekor_types.Dsse,\n    ) -> TransparencyLogEntry | None:\n        \"\"\"\n        Retrieves an extant Rekor entry, identified by its artifact signature,\n        artifact hash, and signing certificate.\n\n        Returns None if Rekor has no entry corresponding to the signing\n        materials.\n        \"\"\"\n        data = {\"entries\": [expected_entry.model_dump(mode=\"json\", by_alias=True)]}\n\n        resp: requests.Response = self.session.post(self.url, json=data)\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            if http_error.response and http_error.response.status_code == 404:\n                return None\n            raise RekorClientError(http_error)\n\n        results = resp.json()\n\n        # The response is a list of `{uuid: LogEntry}` objects.\n        # We select the oldest entry for our actual return value,\n        # since a malicious actor could conceivably spam the log with\n        # newer duplicate entries.\n        oldest_entry: TransparencyLogEntry | None = None\n        for result in results:\n            entry = TransparencyLogEntry._from_v1_response(result)\n\n            # We expect every entry in Rekor v1 to have an integrated time.\n            if entry._inner.integrated_time is None:\n                raise ValueError(\n                    f\"Rekor v1 gave us an entry without an integrated time: {entry._inner.log_index}\"\n                )\n\n            if (\n                oldest_entry is None\n                or entry._inner.integrated_time < oldest_entry._inner.integrated_time  # type: ignore[operator]\n            ):\n                oldest_entry = entry\n\n        return oldest_entry\n\n\nclass RekorClient(RekorLogSubmitter):\n    \"\"\"The internal Rekor client\"\"\"\n\n    def __init__(self, url: str) -> None:\n        \"\"\"\n        Create a new `RekorClient` from the given URL.\n        \"\"\"\n        self.url = f\"{url}/api/v1\"\n        self._thread_local = threading.local()\n\n    @classmethod\n    def production(cls) -> RekorClient:\n        \"\"\"\n        Returns a `RekorClient` populated with the default Rekor production instance.\n        \"\"\"\n        return cls(\n            DEFAULT_REKOR_URL,\n        )\n\n    @classmethod\n    def staging(cls) -> RekorClient:\n        \"\"\"\n        Returns a `RekorClient` populated with the default Rekor staging instance.\n        \"\"\"\n        return cls(STAGING_REKOR_URL)\n\n    @property\n    def _session(self) -> requests.Session:\n        \"\"\"\n        Lazy-initialized thread-local session object\n        \"\"\"\n        if not hasattr(self._thread_local, \"session\"):\n            session = requests.Session()\n            session.headers.update(\n                {\n                    \"Content-Type\": \"application/json\",\n                    \"Accept\": \"application/json\",\n                    \"User-Agent\": USER_AGENT,\n                }\n            )\n            self._thread_local.session = session\n        return self._thread_local.session  # type: ignore[no-any-return]\n\n    @property\n    def log(self) -> RekorLog:\n        \"\"\"\n        Returns a `RekorLog` adapter for making requests to a Rekor log.\n        \"\"\"\n\n        return RekorLog(f\"{self.url}/log\", session=self._session)\n\n    def create_entry(self, request: EntryRequestBody) -> TransparencyLogEntry:\n        \"\"\"\n        Submit the request to Rekor.\n\n        create_entry() can be called from multiple threads.\n        \"\"\"\n        return self.log.entries.post(request)\n\n    def _build_hashed_rekord_request(  # type: ignore[override]\n        self, hashed_input: Hashed, signature: bytes, certificate: Certificate\n    ) -> EntryRequestBody:\n        \"\"\"\n        Construct a hashed rekord payload to submit to Rekor.\n        \"\"\"\n        rekord = rekor_types.Hashedrekord(\n            spec=rekor_types.hashedrekord.HashedrekordV001Schema(\n                signature=rekor_types.hashedrekord.Signature(\n                    content=base64.b64encode(signature).decode(),\n                    public_key=rekor_types.hashedrekord.PublicKey(\n                        content=base64.b64encode(\n                            certificate.public_bytes(\n                                encoding=serialization.Encoding.PEM\n                            )\n                        ).decode()\n                    ),\n                ),\n                data=rekor_types.hashedrekord.Data(\n                    hash=rekor_types.hashedrekord.Hash(\n                        algorithm=hashed_input._as_hashedrekord_algorithm(),\n                        value=hashed_input.digest.hex(),\n                    )\n                ),\n            ),\n        )\n        return EntryRequestBody(rekord.model_dump(mode=\"json\", by_alias=True))\n\n    def _build_dsse_request(  # type: ignore[override]\n        self, envelope: Envelope, certificate: Certificate\n    ) -> EntryRequestBody:\n        \"\"\"\n        Construct a dsse request to submit to Rekor.\n        \"\"\"\n        dsse = rekor_types.Dsse(\n            spec=rekor_types.dsse.DsseSchema(\n                # NOTE: mypy can't see that this kwarg is correct due to two interacting\n                # behaviors/bugs (one pydantic, one datamodel-codegen):\n                # See: <https://github.com/pydantic/pydantic/discussions/7418#discussioncomment-9024927>\n                # See: <https://github.com/koxudaxi/datamodel-code-generator/issues/1903>\n                proposed_content=rekor_types.dsse.ProposedContent(  # type: ignore[call-arg]\n                    envelope=envelope.to_json(),\n                    verifiers=[\n                        base64.b64encode(\n                            certificate.public_bytes(\n                                encoding=serialization.Encoding.PEM\n                            )\n                        ).decode()\n                    ],\n                ),\n            ),\n        )\n        return EntryRequestBody(dsse.model_dump(mode=\"json\", by_alias=True))\n"
  },
  {
    "path": "sigstore/_internal/rekor/client_v2.py",
    "content": "# Copyright 2025 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nClient implementation for interacting with Rekor v2.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport json\nimport logging\nimport threading\n\nimport requests\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.x509 import Certificate\nfrom sigstore_models.common import v1 as common_v1\nfrom sigstore_models.rekor import v2 as rekor_v2\nfrom sigstore_models.rekor.v1 import TransparencyLogEntry as _TransparencyLogEntry\n\nfrom sigstore._internal import USER_AGENT\nfrom sigstore._internal.key_details import _get_key_details\nfrom sigstore._internal.rekor import (\n    EntryRequestBody,\n    RekorClientError,\n    RekorLogSubmitter,\n)\nfrom sigstore.dsse import Envelope\nfrom sigstore.hashes import Hashed\nfrom sigstore.models import TransparencyLogEntry\n\n_logger = logging.getLogger(__name__)\n\n\nclass RekorV2Client(RekorLogSubmitter):\n    \"\"\"\n    The internal Rekor client for the v2 API.\n\n    See https://github.com/sigstore/rekor-tiles/blob/main/CLIENTS.md\n    \"\"\"\n\n    def __init__(self, base_url: str) -> None:\n        \"\"\"\n        Create a new `RekorV2Client` from the given URL.\n        \"\"\"\n        self.url = f\"{base_url}/api/v2\"\n        self._thread_local = threading.local()\n\n    @property\n    def _session(self) -> requests.Session:\n        \"\"\"\n        Lazy-initialized thread-local session object\n        \"\"\"\n        if not hasattr(self._thread_local, \"session\"):\n            session = requests.Session()\n            session.headers.update(\n                {\n                    \"Content-Type\": \"application/json\",\n                    \"Accept\": \"application/json\",\n                    \"User-Agent\": USER_AGENT,\n                }\n            )\n            self._thread_local.session = session\n        return self._thread_local.session  # type: ignore[no-any-return]\n\n    def create_entry(self, payload: EntryRequestBody) -> TransparencyLogEntry:\n        \"\"\"\n        Submit a new entry for inclusion in the Rekor log.\n\n        Note that this call can take a fairly long time as the log\n        only responds after the entry has been included in the log.\n        https://github.com/sigstore/rekor-tiles/blob/main/CLIENTS.md#handling-longer-requests\n\n        create_entry() can be called from multiple threads.\n        \"\"\"\n        _logger.debug(f\"proposed: {json.dumps(payload)}\")\n\n        resp = self._session.post(\n            f\"{self.url}/log/entries\",\n            json=payload,\n        )\n\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            raise RekorClientError(http_error)\n\n        integrated_entry = resp.json()\n        _logger.debug(f\"integrated: {integrated_entry}\")\n        inner = _TransparencyLogEntry.from_dict(integrated_entry)\n        return TransparencyLogEntry(inner)\n\n    @classmethod\n    def _build_hashed_rekord_request(\n        cls,\n        hashed_input: Hashed,\n        signature: bytes,\n        certificate: Certificate,\n    ) -> EntryRequestBody:\n        \"\"\"\n        Construct a hashed rekord request to submit to Rekor.\n        \"\"\"\n        req = rekor_v2.entry.CreateEntryRequest(\n            hashed_rekord_request_v002=rekor_v2.hashedrekord.HashedRekordRequestV002(\n                digest=base64.b64encode(hashed_input.digest),\n                signature=rekor_v2.verifier.Signature(\n                    content=base64.b64encode(signature),\n                    verifier=rekor_v2.verifier.Verifier(\n                        x509_certificate=common_v1.X509Certificate(\n                            raw_bytes=base64.b64encode(\n                                certificate.public_bytes(\n                                    encoding=serialization.Encoding.DER\n                                )\n                            )\n                        ),\n                        key_details=_get_key_details(certificate),\n                    ),\n                ),\n            )\n        )\n        return EntryRequestBody(req.to_dict())\n\n    @classmethod\n    def _build_dsse_request(\n        cls, envelope: Envelope, certificate: Certificate\n    ) -> EntryRequestBody:\n        \"\"\"\n        Construct a dsse request to submit to Rekor.\n        \"\"\"\n        req = rekor_v2.entry.CreateEntryRequest(\n            dsse_request_v002=rekor_v2.dsse.DSSERequestV002(\n                envelope=envelope._inner,\n                verifiers=[\n                    rekor_v2.verifier.Verifier(\n                        x509_certificate=common_v1.X509Certificate(\n                            raw_bytes=base64.b64encode(\n                                certificate.public_bytes(\n                                    encoding=serialization.Encoding.DER\n                                )\n                            )\n                        ),\n                        key_details=_get_key_details(certificate),\n                    )\n                ],\n            )\n        )\n        return EntryRequestBody(req.to_dict())\n"
  },
  {
    "path": "sigstore/_internal/sct.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nUtilities for verifying signed certificate timestamps.\n\"\"\"\n\nimport logging\nimport struct\nfrom datetime import timezone\n\nfrom cryptography.hazmat.primitives import hashes, serialization\nfrom cryptography.hazmat.primitives.asymmetric import ec, rsa\nfrom cryptography.x509 import (\n    Certificate,\n    ExtendedKeyUsage,\n    ExtensionNotFound,\n    PrecertificateSignedCertificateTimestamps,\n)\nfrom cryptography.x509.certificate_transparency import (\n    LogEntryType,\n    SignedCertificateTimestamp,\n)\nfrom cryptography.x509.oid import ExtendedKeyUsageOID\n\nfrom sigstore._internal.trust import CTKeyring\nfrom sigstore._utils import (\n    KeyID,\n    cert_is_ca,\n    key_id,\n)\nfrom sigstore.errors import VerificationError\n\n_logger = logging.getLogger(__name__)\n\n\ndef _pack_signed_entry(\n    sct: SignedCertificateTimestamp, cert: Certificate, issuer_key_id: bytes | None\n) -> bytes:\n    fields = []\n    if sct.entry_type == LogEntryType.X509_CERTIFICATE:\n        # When dealing with a \"normal\" certificate, our signed entry looks like this:\n        #\n        # [0]: opaque ASN.1Cert<1..2^24-1>\n        pack_format = \"!BBB{cert_der_len}s\"\n        cert_der = cert.public_bytes(encoding=serialization.Encoding.DER)\n    elif sct.entry_type == LogEntryType.PRE_CERTIFICATE:\n        if not issuer_key_id or len(issuer_key_id) != 32:\n            raise VerificationError(\"API misuse: issuer key ID missing\")\n\n        # When dealing with a precertificate, our signed entry looks like this:\n        #\n        # [0]: issuer_key_id[32]\n        # [1]: opaque TBSCertificate<1..2^24-1>\n        pack_format = \"!32sBBB{cert_der_len}s\"\n\n        # Precertificates must have their SCT list extension filtered out.\n        cert_der = cert.tbs_precertificate_bytes\n        fields.append(issuer_key_id)\n    else:\n        raise VerificationError(f\"unknown SCT log entry type: {sct.entry_type!r}\")\n\n    # The `opaque` length is a u24, which isn't directly supported by `struct`.\n    # So we have to decompose it into 3 bytes.\n    unused, len1, len2, len3 = struct.unpack(\n        \"!4B\",\n        struct.pack(\"!I\", len(cert_der)),\n    )\n    if unused:\n        raise VerificationError(\n            f\"Unexpectedly large certificate length: {len(cert_der)}\"\n        )\n\n    pack_format = pack_format.format(cert_der_len=len(cert_der))\n    fields.extend((len1, len2, len3, cert_der))\n\n    return struct.pack(pack_format, *fields)\n\n\ndef _pack_digitally_signed(\n    sct: SignedCertificateTimestamp,\n    cert: Certificate,\n    issuer_key_id: KeyID | None,\n) -> bytes:\n    \"\"\"\n    Packs the contents of `cert` (and some pieces of `sct`) into a structured\n    blob, one that forms the signature body of the \"digitally-signed\" struct\n    for an SCT.\n\n    The format of the digitally signed data is described in IETF's RFC 6962.\n    \"\"\"\n\n    # This constructs the \"core\" `signed_entry` field, which is either\n    # the public bytes of the cert *or* the TBSPrecertificate (with some\n    # filtering), depending on whether our SCT is for a precertificate.\n    signed_entry = _pack_signed_entry(sct, cert, issuer_key_id)\n\n    # Assemble a format string with the certificate length baked in and then pack the digitally\n    # signed data\n    # fmt: off\n    pattern = f\"!BBQH{len(signed_entry)}sH{len(sct.extension_bytes)}s\"\n    timestamp = sct.timestamp.replace(tzinfo=timezone.utc)\n    data = struct.pack(\n        pattern,\n        sct.version.value,                  # sct_version\n        0,                                  # signature_type (certificate_timestamp(0))\n        int(timestamp.timestamp() * 1000),  # timestamp (milliseconds)\n        sct.entry_type.value,               # entry_type (x509_entry(0) | precert_entry(1))\n        signed_entry,                       # select(entry_type) -> signed_entry (see above)\n        len(sct.extension_bytes),           # extensions (opaque CtExtensions<0..2^16-1>)\n        sct.extension_bytes,\n    )\n    # fmt: on\n\n    return data\n\n\ndef _is_preissuer(issuer: Certificate) -> bool:\n    try:\n        ext_key_usage = issuer.extensions.get_extension_for_class(ExtendedKeyUsage)\n    # If we do not have any EKU, we certainly do not have CT Ext\n    except ExtensionNotFound:\n        return False\n\n    return ExtendedKeyUsageOID.CERTIFICATE_TRANSPARENCY in ext_key_usage.value\n\n\ndef _get_issuer_cert(chain: list[Certificate]) -> Certificate:\n    issuer = chain[0]\n    if _is_preissuer(issuer):\n        issuer = chain[1]\n    return issuer\n\n\ndef _get_signed_certificate_timestamp(\n    certificate: Certificate,\n) -> SignedCertificateTimestamp:\n    \"\"\"Retrieve the embedded SCT from the certificate.\n\n    Raise VerificationError if certificate does not contain exactly one SCT\n    \"\"\"\n    try:\n        timestamps = certificate.extensions.get_extension_for_class(\n            PrecertificateSignedCertificateTimestamps\n        ).value\n    except ExtensionNotFound:\n        raise VerificationError(\n            \"Certificate does not contain a signed certificate timestamp extension\"\n        )\n\n    if len(timestamps) != 1:\n        raise VerificationError(\n            f\"Expected one certificate timestamp, found {len(timestamps)}\"\n        )\n    sct: SignedCertificateTimestamp = timestamps[0]\n    return sct\n\n\ndef _cert_is_ca(cert: Certificate) -> bool:\n    _logger.debug(f\"Found {cert.subject} as issuer, verifying if it is a ca\")\n    try:\n        cert_is_ca(cert)\n    except VerificationError as e:\n        _logger.debug(f\"Invalid {cert.subject}: failed to validate as a CA: {e}\")\n        return False\n    return True\n\n\ndef verify_sct(\n    cert: Certificate,\n    chain: list[Certificate],\n    ct_keyring: CTKeyring,\n) -> None:\n    \"\"\"\n    Verify a signed certificate timestamp.\n\n    An SCT is verified by reconstructing its \"digitally-signed\" payload\n    and verifying that the signature provided in the SCT is valid against\n    one of the keys present in the CT keyring (i.e., the keys used by the CT\n    log to sign SCTs).\n    \"\"\"\n\n    sct = _get_signed_certificate_timestamp(cert)\n\n    issuer_key_id = None\n    if sct.entry_type == LogEntryType.PRE_CERTIFICATE:\n        # If we're verifying an SCT for a precertificate, we need to\n        # find its issuer in the chain and calculate a hash over\n        # its public key information, as part of the \"binding\" proof\n        # that ties the issuer to the final certificate.\n        issuer_cert = _get_issuer_cert(chain)\n        issuer_pubkey = issuer_cert.public_key()\n\n        if not _cert_is_ca(issuer_cert):\n            raise VerificationError(\n                f\"SCT verify: Invalid issuer pubkey basicConstraint (not a CA): {issuer_pubkey}\"\n            )\n\n        if not isinstance(issuer_pubkey, rsa.RSAPublicKey | ec.EllipticCurvePublicKey):\n            raise VerificationError(\n                f\"SCT verify: invalid issuer pubkey format (not ECDSA or RSA): {issuer_pubkey}\"\n            )\n\n        issuer_key_id = key_id(issuer_pubkey)\n\n    digitally_signed = _pack_digitally_signed(sct, cert, issuer_key_id)\n\n    if not isinstance(sct.signature_hash_algorithm, hashes.SHA256):\n        raise VerificationError(\n            \"Found unexpected hash algorithm in SCT: only SHA256 is supported \"\n            f\"(expected {hashes.SHA256}, got {sct.signature_hash_algorithm})\"\n        )\n\n    try:\n        _logger.debug(f\"attempting to verify SCT with key ID {sct.log_id.hex()}\")\n        # NOTE(ww): In terms of the DER structure, the SCT's `LogID` contains a\n        # singular `opaque key_id[32]`. Cryptography's APIs don't bother\n        # to expose this trivial single member, so we use the `log_id`\n        # attribute directly.\n        ct_keyring.verify(\n            key_id=KeyID(sct.log_id), signature=sct.signature, data=digitally_signed\n        )\n    except VerificationError as exc:\n        raise VerificationError(f\"SCT verify failed: {exc}\")\n"
  },
  {
    "path": "sigstore/_internal/timestamp.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nUtilities to deal with sources of signed time.\n\"\"\"\n\nimport enum\nimport threading\nfrom dataclasses import dataclass\nfrom datetime import datetime\n\nimport requests\nfrom rfc3161_client import (\n    TimestampRequestBuilder,\n    TimeStampResponse,\n    decode_timestamp_response,\n)\nfrom rfc3161_client.base import HashAlgorithm\n\nfrom sigstore._internal import USER_AGENT\n\nCLIENT_TIMEOUT: int = 5\n\n\nclass TimestampSource(enum.Enum):\n    \"\"\"Represents the source of a timestamp.\"\"\"\n\n    TIMESTAMP_AUTHORITY = enum.auto()\n    TRANSPARENCY_SERVICE = enum.auto()\n\n\n@dataclass\nclass TimestampVerificationResult:\n    \"\"\"Represents a timestamp used by the Verifier.\n\n    A Timestamp either comes from a Timestamping Service (RFC3161) or the Transparency\n    Service.\n    \"\"\"\n\n    source: TimestampSource\n    time: datetime\n\n\nclass TimestampError(Exception):\n    \"\"\"\n    A generic error in the TimestampAuthority client.\n    \"\"\"\n\n    pass\n\n\nclass TimestampAuthorityClient:\n    \"\"\"Internal client to deal with a Timestamp Authority\"\"\"\n\n    def __init__(self, url: str) -> None:\n        \"\"\"\n        Create a new `TimestampAuthorityClient` from the given URL.\n        \"\"\"\n        self.url = url\n        self._thread_local = threading.local()\n\n    @property\n    def _session(self) -> requests.Session:\n        \"\"\"\n        Lazy-initialized thread-local session object\n        \"\"\"\n        if not hasattr(self._thread_local, \"session\"):\n            session = requests.Session()\n            session.headers.update(\n                {\n                    \"Content-Type\": \"application/timestamp-query\",\n                    \"User-Agent\": USER_AGENT,\n                }\n            )\n            self._thread_local.session = session\n        return self._thread_local.session  # type: ignore[no-any-return]\n\n    def request_timestamp(self, signature: bytes) -> TimeStampResponse:\n        \"\"\"\n        Timestamp the signature using the configured Timestamp Authority.\n\n        This method generates a RFC3161 Timestamp Request and sends it to a TSA.\n        The received response is parsed but *not* cryptographically verified.\n\n        request_timestamp() can be called from multiple threads.\n\n        Raises a TimestampError on failure.\n        \"\"\"\n        # Build the timestamp request\n        try:\n            timestamp_request = (\n                TimestampRequestBuilder()\n                .hash_algorithm(HashAlgorithm.SHA256)\n                .data(signature)\n                .nonce(nonce=True)\n                .build()\n            )\n        except ValueError as error:\n            msg = f\"invalid request: {error}\"\n            raise TimestampError(msg)\n\n        # Send it to the TSA for signing\n        try:\n            response = self._session.post(\n                self.url,\n                data=timestamp_request.as_bytes(),\n                timeout=CLIENT_TIMEOUT,\n            )\n            response.raise_for_status()\n        except requests.RequestException as error:\n            msg = f\"error while sending the request to the TSA: {error}\"\n            raise TimestampError(msg)\n\n        # Check that we can parse the response but do not *verify* it\n        try:\n            timestamp_response = decode_timestamp_response(response.content)\n        except ValueError as e:\n            msg = f\"invalid response: {e}\"\n            raise TimestampError(msg)\n\n        return timestamp_response\n"
  },
  {
    "path": "sigstore/_internal/trust.py",
    "content": "# Copyright 2023 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nClient trust configuration and trust root management for sigstore-python.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import ClassVar, NewType\n\nimport cryptography.hazmat.primitives.asymmetric.padding as padding\nfrom cryptography.exceptions import InvalidSignature\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa\nfrom cryptography.x509 import (\n    Certificate,\n    load_der_x509_certificate,\n)\nfrom sigstore_models.common import v1 as common_v1\nfrom sigstore_models.trustroot import v1 as trustroot_v1\n\nfrom sigstore._utils import (\n    KeyID,\n    PublicKey,\n    is_timerange_valid,\n    key_id,\n    load_der_public_key,\n)\nfrom sigstore.errors import Error, VerificationError\n\n# Versions supported by this client\nREKOR_VERSIONS = [1, 2]\nTSA_VERSIONS = [1]\nFULCIO_VERSIONS = [1]\nOIDC_VERSIONS = [1]\n\n_logger = logging.getLogger(__name__)\n\n\n@dataclass(init=False)\nclass Key:\n    \"\"\"\n    Represents a key in a `Keyring`.\n    \"\"\"\n\n    hash_algorithm: hashes.HashAlgorithm | None\n    key: PublicKey\n    key_id: KeyID\n\n    _RSA_SHA_256_DETAILS: ClassVar = {\n        common_v1.PublicKeyDetails.PKCS1_RSA_PKCS1V5,\n        common_v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256,\n        common_v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256,\n        common_v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256,\n    }\n\n    _EC_DETAILS_TO_HASH: ClassVar = {\n        common_v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256: hashes.SHA256(),\n        common_v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384: hashes.SHA384(),\n        common_v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512: hashes.SHA512(),\n    }\n\n    def __init__(self, public_key: common_v1.PublicKey) -> None:\n        \"\"\"\n        Construct a key from the given Sigstore PublicKey message.\n        \"\"\"\n\n        # NOTE: `raw_bytes` is marked as `optional` in the `PublicKey` message,\n        # for unclear reasons.\n        if not public_key.raw_bytes:\n            raise VerificationError(\"public key is empty\")\n\n        hash_algorithm: hashes.HashAlgorithm | None\n        if public_key.key_details in self._RSA_SHA_256_DETAILS:\n            hash_algorithm = hashes.SHA256()\n            key = load_der_public_key(public_key.raw_bytes, types=(rsa.RSAPublicKey,))\n        elif public_key.key_details in self._EC_DETAILS_TO_HASH:\n            hash_algorithm = self._EC_DETAILS_TO_HASH[public_key.key_details]\n            key = load_der_public_key(\n                public_key.raw_bytes, types=(ec.EllipticCurvePublicKey,)\n            )\n        elif public_key.key_details == common_v1.PublicKeyDetails.PKIX_ED25519:\n            hash_algorithm = None\n            key = load_der_public_key(\n                public_key.raw_bytes, types=(ed25519.Ed25519PublicKey,)\n            )\n        else:\n            raise VerificationError(f\"unsupported key type: {public_key.key_details}\")\n\n        self.hash_algorithm = hash_algorithm\n        self.key = key\n        self.key_id = key_id(key)\n\n    def verify(self, signature: bytes, data: bytes) -> None:\n        \"\"\"\n        Verifies the given `data` against `signature` using the current key.\n        \"\"\"\n        if isinstance(self.key, rsa.RSAPublicKey) and self.hash_algorithm is not None:\n            self.key.verify(\n                signature=signature,\n                data=data,\n                # TODO: Parametrize this as well, for PSS.\n                padding=padding.PKCS1v15(),\n                algorithm=self.hash_algorithm,\n            )\n        elif (\n            isinstance(self.key, ec.EllipticCurvePublicKey)\n            and self.hash_algorithm is not None\n        ):\n            self.key.verify(\n                signature=signature,\n                data=data,\n                signature_algorithm=ec.ECDSA(self.hash_algorithm),\n            )\n        elif (\n            isinstance(self.key, ed25519.Ed25519PublicKey)\n            and self.hash_algorithm is None\n        ):\n            self.key.verify(\n                signature=signature,\n                data=data,\n            )\n        else:\n            # Unreachable without API misuse.\n            raise VerificationError(f\"keyring: unsupported key: {self.key}\")\n\n\nclass Keyring:\n    \"\"\"\n    Represents a set of keys, each of which is a potentially valid verifier.\n    \"\"\"\n\n    def __init__(self, public_keys: list[common_v1.PublicKey] = []):\n        \"\"\"\n        Create a new `Keyring`, with `keys` as the initial set of verifying keys.\n        \"\"\"\n        self._keyring: dict[KeyID, Key] = {}\n\n        for public_key in public_keys:\n            try:\n                key = Key(public_key)\n                self._keyring[key.key_id] = key\n            except VerificationError as e:\n                _logger.warning(f\"Failed to load a trusted root key: {e}\")\n\n    def verify(self, *, key_id: KeyID, signature: bytes, data: bytes) -> None:\n        \"\"\"\n        Verify that `signature` is a valid signature for `data`, using the\n        key identified by `key_id`.\n\n        `key_id` is an unauthenticated hint; if no key matches the given key ID,\n        all keys in the keyring are tried.\n\n        Raises if the signature is invalid, i.e. is not valid for any of the\n        keys in the keyring.\n        \"\"\"\n\n        key = self._keyring.get(key_id)\n        candidates = [key] if key is not None else list(self._keyring.values())\n\n        # Try to verify each candidate key. In the happy case, this will\n        # be exactly one candidate.\n        valid = False\n        for candidate in candidates:\n            try:\n                candidate.verify(signature, data)\n                valid = True\n                break\n            except InvalidSignature:\n                pass\n\n        if not valid:\n            raise VerificationError(\"keyring: invalid signature\")\n\n\nRekorKeyring = NewType(\"RekorKeyring\", Keyring)\nCTKeyring = NewType(\"CTKeyring\", Keyring)\n\n\nclass KeyringPurpose(str, Enum):\n    \"\"\"\n    Keyring purpose typing\n    \"\"\"\n\n    SIGN = \"sign\"\n    VERIFY = \"verify\"\n\n    def __str__(self) -> str:\n        \"\"\"Returns the purpose string value.\"\"\"\n        return self.value\n\n\nclass CertificateAuthority:\n    \"\"\"\n    Certificate Authority used in a Trusted Root configuration.\n    \"\"\"\n\n    def __init__(self, inner: trustroot_v1.CertificateAuthority):\n        \"\"\"\n        Construct a new `CertificateAuthority`.\n\n        @api private\n        \"\"\"\n        self._inner = inner\n        self._certificates: list[Certificate] = []\n        self._verify()\n\n    @classmethod\n    def from_json(cls, path: str) -> CertificateAuthority:\n        \"\"\"\n        Create a CertificateAuthority directly from JSON.\n        \"\"\"\n        inner = trustroot_v1.CertificateAuthority.from_json(Path(path).read_bytes())\n        return cls(inner)\n\n    def _verify(self) -> None:\n        \"\"\"\n        Verify and load the certificate authority.\n        \"\"\"\n        self._certificates = [\n            load_der_x509_certificate(cert.raw_bytes)\n            for cert in self._inner.cert_chain.certificates\n        ]\n\n        if not self._certificates:\n            raise Error(\"missing a certificate in Certificate Authority\")\n\n    @property\n    def validity_period_start(self) -> datetime:\n        \"\"\"\n        Validity period start.\n        \"\"\"\n        return self._inner.valid_for.start\n\n    @property\n    def validity_period_end(self) -> datetime | None:\n        \"\"\"\n        Validity period end.\n        \"\"\"\n        return self._inner.valid_for.end\n\n    def certificates(self, *, allow_expired: bool) -> list[Certificate]:\n        \"\"\"\n        Return a list of certificates in the authority chain.\n\n        The certificates are returned in order from leaf to root, with any\n        intermediate certificates in between.\n        \"\"\"\n        if not is_timerange_valid(self._inner.valid_for, allow_expired=allow_expired):\n            return []\n        return self._certificates\n"
  },
  {
    "path": "sigstore/_internal/tuf.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nTUF functionality for `sigstore-python`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom urllib import parse\n\nimport platformdirs\nfrom tuf.api import exceptions as TUFExceptions\nfrom tuf.ngclient import Updater, UpdaterConfig  # type: ignore[attr-defined]\n\nfrom sigstore import __version__\nfrom sigstore._utils import read_embedded\nfrom sigstore.errors import TUFError\n\n_logger = logging.getLogger(__name__)\n\nDEFAULT_TUF_URL = \"https://tuf-repo-cdn.sigstore.dev\"\nSTAGING_TUF_URL = \"https://tuf-repo-cdn.sigstage.dev\"\n\n\ndef _get_dirs(url: str) -> tuple[Path, Path]:\n    \"\"\"\n    Given a TUF repository URL, return suitable local metadata and cache directories.\n\n    These directories are not guaranteed to already exist.\n    \"\"\"\n\n    app_name = \"sigstore-python\"\n    app_author = \"sigstore\"\n\n    repo_base = parse.quote(url, safe=\"\")\n\n    tuf_data_dir = Path(platformdirs.user_data_dir(app_name, app_author)) / \"tuf\"\n    tuf_cache_dir = Path(platformdirs.user_cache_dir(app_name, app_author)) / \"tuf\"\n\n    return (tuf_data_dir / repo_base), (tuf_cache_dir / repo_base)\n\n\nclass TrustUpdater:\n    \"\"\"Internal trust root (certificates and keys) downloader.\n\n    TrustUpdater discovers the currently valid certificates and keys and\n    securely downloads them from the remote TUF repository at 'url'.\n\n    TrustUpdater expects to find an initial root.json in either the local\n    metadata directory for this URL, or (as special case for the sigstore.dev\n    production and staging instances) in the application resources.\n    \"\"\"\n\n    def __init__(\n        self, url: str, offline: bool = False, bootstrap_root: Path | None = None\n    ) -> None:\n        \"\"\"\n        Create a new `TrustUpdater`, pulling from the given `url`.\n\n        TrustUpdater expects that either embedded data contains\n        a root.json for this url or that `bootstrap_root` is provided as argument.\n\n        If not `offline`, TrustUpdater will update the TUF metadata from\n        the remote repository.\n        \"\"\"\n        # not canonicalization, just handling trailing slash as common mistake:\n        url = url.rstrip(\"/\")\n\n        self._metadata_dir, self._targets_dir = _get_dirs(url)\n\n        # Populate targets cache so we don't have to download these versions\n        self._targets_dir.mkdir(parents=True, exist_ok=True)\n\n        for artifact in [\"trusted_root.json\", \"signing_config.v0.2.json\"]:\n            artifact_path = self._targets_dir / artifact\n            if not artifact_path.exists():\n                try:\n                    data = read_embedded(artifact, url)\n                    artifact_path.write_bytes(data)\n                except FileNotFoundError:\n                    pass  # this is ok: we only have embedded data for specific repos\n\n        _logger.debug(f\"TUF metadata: {self._metadata_dir}\")\n        _logger.debug(f\"TUF targets cache: {self._targets_dir}\")\n\n        self._updater: Updater | None = None\n        if offline:\n            _logger.warning(\n                \"TUF repository is loaded in offline mode; updates will not be performed\"\n            )\n        else:\n            # Initialize and update the toplevel TUF metadata\n            try:\n                root_json: bytes | None = read_embedded(\"root.json\", url)\n            except FileNotFoundError:\n                # We do not have embedded root metadata for this URL: we can still\n                # initialize _if_ given bootstrap root (i.e. during \"sigstore trust-instance\")\n                # or local metadata exists already (after \"sigstore trust-instance\")\n                root_json = bootstrap_root.read_bytes() if bootstrap_root else None\n\n            try:\n                self._updater = Updater(\n                    metadata_dir=str(self._metadata_dir),\n                    metadata_base_url=url,\n                    target_base_url=parse.urljoin(f\"{url}/\", \"targets/\"),\n                    target_dir=str(self._targets_dir),\n                    config=UpdaterConfig(\n                        app_user_agent=f\"sigstore-python/{__version__}\"\n                    ),\n                    bootstrap=root_json,\n                )\n                self._updater.refresh()\n            except Exception as e:\n                raise TUFError(\"Failed to refresh TUF metadata\") from e\n\n    @lru_cache()\n    def get_trusted_root_path(self) -> str:\n        \"\"\"Return local path to currently valid trusted root file\"\"\"\n        if not self._updater:\n            _logger.debug(\"Using unverified trusted root from cache\")\n            return str(self._targets_dir / \"trusted_root.json\")\n\n        root_info = self._updater.get_targetinfo(\"trusted_root.json\")\n        if root_info is None:\n            raise TUFError(\"Unsupported TUF configuration: no trusted root\")\n        path = self._updater.find_cached_target(root_info)\n        if path is None:\n            try:\n                path = self._updater.download_target(root_info)\n            except (\n                TUFExceptions.DownloadError,\n                TUFExceptions.RepositoryError,\n            ) as e:\n                raise TUFError(\"Failed to download trusted key bundle\") from e\n\n        _logger.debug(\"Found and verified trusted root\")\n        return path\n\n    @lru_cache()\n    def get_signing_config_path(self) -> str:\n        \"\"\"Return local path to currently valid signing config file\"\"\"\n        if not self._updater:\n            _logger.debug(\"Using unverified signing config from cache\")\n            return str(self._targets_dir / \"signing_config.v0.2.json\")\n\n        root_info = self._updater.get_targetinfo(\"signing_config.v0.2.json\")\n        if root_info is None:\n            raise TUFError(\"Unsupported TUF configuration: no signing config\")\n        path = self._updater.find_cached_target(root_info)\n        if path is None:\n            try:\n                path = self._updater.download_target(root_info)\n            except (\n                TUFExceptions.DownloadError,\n                TUFExceptions.RepositoryError,\n            ) as e:\n                raise TUFError(\"Failed to download signing config\") from e\n\n        _logger.debug(\"Found and verified signing config\")\n        return path\n"
  },
  {
    "path": "sigstore/_store/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nAn empty module, used to assist Python's resource machinery in embedding\nassets.\n\"\"\"\n\n\n# NOTE: This is arguably incorrect, since _store only contains non-Python files.\n# However, due to how `importlib.resources` is designed, only top-level resources\n# inside of packages or modules can be accessed, so this directory needs to be a\n# module in order for us to programmatically access the keys and root certs in it.\n#\n# Why do we bother with `importlib` at all? Because we might be installed as a\n# ZIP file or an Egg, which in turn means that our resource files don't actually\n# exist separately on disk. `importlib` is the only reliable way to access them.\n"
  },
  {
    "path": "sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/root.json",
    "content": "{\n \"signatures\": [\n  {\n   \"keyid\": \"aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81\",\n   \"sig\": \"3045022100d404545c87d31829c26820dc963389ef8497dbb1a712e08f5e81ce5a92c3ec600220314d108bc9e827c1a67610d1c90d5fb9a426ccc8bde009fb663c292b1728e6a4\"\n  },\n  {\n   \"keyid\": \"61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc\",\n   \"sig\": \"304402204cbe823ca173f04c4fd59cb01941efbd9f2b9452f405a3cd1c5bcb7481a818f902201cb4223b74b8e54f5de44936ae3c7adef32959da8d7d9625d23e464263c39e97\"\n  },\n  {\n   \"keyid\": \"9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237\",\n   \"sig\": \"\"\n  },\n  {\n   \"keyid\": \"0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5\",\n   \"sig\": \"\"\n  }\n ],\n \"signed\": {\n  \"_type\": \"root\",\n  \"consistent_snapshot\": true,\n  \"expires\": \"2026-05-22T19:23:14Z\",\n  \"keys\": {\n   \"0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5\": {\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoxkvDOmtGEknB3M+ZkPts8joDM0X\\nIH5JZwPlgC2CXs/eqOuNF8AcEWwGYRiDhV/IMlQw5bg8PLICQcgsbrDiKg==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@mnm678\"\n   },\n   \"61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc\": {\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE++Wv+DcLRk+mfkmlpCwl1GUi9EMh\\npBUTz8K0fH7bE4mQuViGSyWA/eyMc0HvzZi6Xr0diHw0/lUPBvok214YQw==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@kommendorkapten\"\n   },\n   \"9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237\": {\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFHDb85JH+JYR1LQmxiz4UMokVMnP\\nxKoWpaEnFCKXH8W4Fc/DfIxMnkpjCuvWUBdJXkO0aDIxwsij8TOFh2R7dw==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@joshuagl\"\n   },\n   \"aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81\": {\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEohqIdE+yTl4OxpX8ZxNUPrg3SL9H\\nBDnhZuceKkxy2oMhUOxhWweZeG3bfM1T4ZLnJimC6CAYVU5+F5jZCoftRw==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@jku\"\n   },\n   \"c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4\": {\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExxmEtmhF5U+i+v/6he4BcSLzCgMx\\n/0qSrvDg6bUWwUrkSKS2vDpcJrhGy5fmmhRrGawjPp1ALpC3y1kqFTpXDg==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-online-uri\": \"gcpkms:projects/projectsigstore-staging/locations/global/keyRings/tuf-keyring/cryptoKeys/tuf-key/cryptoKeyVersions/2\"\n   }\n  },\n  \"roles\": {\n   \"root\": {\n    \"keyids\": [\n     \"aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81\",\n     \"61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc\",\n     \"9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237\",\n     \"0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5\"\n    ],\n    \"threshold\": 2\n   },\n   \"snapshot\": {\n    \"keyids\": [\n     \"c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4\"\n    ],\n    \"threshold\": 1,\n    \"x-tuf-on-ci-expiry-period\": 3650,\n    \"x-tuf-on-ci-signing-period\": 365\n   },\n   \"targets\": {\n    \"keyids\": [\n     \"aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81\",\n     \"61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc\",\n     \"9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237\",\n     \"0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5\"\n    ],\n    \"threshold\": 1\n   },\n   \"timestamp\": {\n    \"keyids\": [\n     \"c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4\"\n    ],\n    \"threshold\": 1,\n    \"x-tuf-on-ci-expiry-period\": 7,\n    \"x-tuf-on-ci-signing-period\": 6\n   }\n  },\n  \"spec_version\": \"1.0\",\n  \"version\": 13,\n  \"x-tuf-on-ci-expiry-period\": 182,\n  \"x-tuf-on-ci-signing-period\": 35\n }\n}"
  },
  {
    "path": "sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/signing_config.v0.2.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n  \"caUrls\": [\n    {\n      \"url\": \"https://fulcio.sigstage.dev\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2022-04-14T21:38:40Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"oidcUrls\": [\n    {\n      \"url\": \"https://oauth2.sigstage.dev/auth\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-16T00:00:00Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"rekorTlogUrls\": [\n    {\n      \"url\": \"https://log2025-alpha3.rekor.sigstage.dev\",\n      \"majorApiVersion\": 2,\n      \"validFor\": {\n        \"start\": \"2025-09-22T11:00:00Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    },\n    {\n      \"url\": \"https://log2025-alpha2.rekor.sigstage.dev\",\n      \"majorApiVersion\": 2,\n      \"validFor\": {\n        \"start\": \"2025-08-20T07:24:08Z\",\n        \"end\": \"2025-09-22T11:00:00Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    },\n    {\n      \"url\": \"https://log2025-alpha1.rekor.sigstage.dev\",\n      \"majorApiVersion\": 2,\n      \"validFor\": {\n        \"start\": \"2025-05-07T12:00:00Z\",\n        \"end\": \"2025-08-20T07:24:08Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    },\n    {\n      \"url\": \"https://rekor.sigstage.dev\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2021-01-12T11:53:27Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"tsaUrls\": [\n    {\n      \"url\": \"https://timestamp.sigstage.dev/api/v1/timestamp\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-09T00:00:00Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"rekorTlogConfig\": {\n    \"selector\": \"ANY\"\n  },\n  \"tsaConfig\": {\n    \"selector\": \"ANY\"\n  }\n}\n"
  },
  {
    "path": "sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/trusted_root.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n  \"tlogs\": [\n    {\n      \"baseUrl\": \"https://rekor.sigstage.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2021-01-12T11:53:27Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://log2025-alpha1.rekor.sigstage.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MCowBQYDK2VwAyEAPn+AREHoBaZ7wgS1zBqpxmLSGnyhxXj4lFxSdWVB8o8=\",\n        \"keyDetails\": \"PKIX_ED25519\",\n        \"validFor\": {\n          \"start\": \"2025-04-16T00:00:00Z\",\n          \"end\": \"2025-09-04T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://log2025-alpha2.rekor.sigstage.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MCowBQYDK2VwAyEAkrA8Ou2FtN7kYXCP/lpvF8vQrvh4nj+91+PWOGGzfGc=\",\n        \"keyDetails\": \"PKIX_ED25519\",\n        \"validFor\": {\n          \"start\": \"2025-08-08T00:00:00Z\",\n          \"end\": \"2025-09-28T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"KfSiSX2iRLyhK62SUVL47vVcqqRx/RAewpKJm8IdZTo=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://log2025-alpha3.rekor.sigstage.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MCowBQYDK2VwAyEAlD3dVc8yaP25mPtT/sJ59D3LLxGBgW/qYrM6x6KmOqk=\",\n        \"keyDetails\": \"PKIX_ED25519\",\n        \"validFor\": {\n          \"start\": \"2025-09-22T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"09OnDKEw7/hpZiYVPoTRzRbglHk0sylsUovegnRUlJY=\"\n      }\n    }\n  ],\n  \"certificateAuthorities\": [\n    {\n      \"subject\": {\n        \"organization\": \"sigstore.dev\",\n        \"commonName\": \"sigstore\"\n      },\n      \"uri\": \"https://fulcio.sigstage.dev\",\n      \"certChain\": {\n        \"certificates\": [\n          {\n            \"rawBytes\": \"MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==\"\n          },\n          {\n            \"rawBytes\": \"MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=\"\n          }\n        ]\n      },\n      \"validFor\": {\n        \"start\": \"2022-04-14T21:38:40Z\"\n      }\n    }\n  ],\n  \"ctlogs\": [\n    {\n      \"baseUrl\": \"https://ctfe.sigstage.dev/test\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==\",\n        \"keyDetails\": \"PKCS1_RSA_PKCS1V5\",\n        \"validFor\": {\n          \"start\": \"2021-03-14T00:00:00Z\",\n          \"end\": \"2022-07-31T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://ctfe.sigstage.dev/2022\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2022-07-01T00:00:00Z\",\n          \"end\": \"2022-07-31T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://ctfe.sigstage.dev/2022-2\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2022-07-01T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=\"\n      }\n    }\n  ],\n  \"timestampAuthorities\": [\n    {\n      \"subject\": {\n        \"organization\": \"sigstore.dev\",\n        \"commonName\": \"sigstore-tsa-selfsigned\"\n      },\n      \"uri\": \"https://timestamp.sigstage.dev/api/v1/timestamp\",\n      \"certChain\": {\n        \"certificates\": [\n          {\n            \"rawBytes\": \"MIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6\"\n          },\n          {\n            \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUCPExEFKiQh0dP4sp5ltmSYSSkFUwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATt0tIDWyo4ARfL9BaSo0W5bJQEbKJTU/u7llvdjSI5aTkOAJa8tixn2+LEfPG4dMFdsMPtsIuU1qn2OqFiuMk6vHv/c+az25RQVY1oo50iMb0jIL3N4FgwhPFpZnCbQPOjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAKBggqhkjOPQQDAwNpADBmAjEA2MI1VXgbf3dUOSc95hSRypBKOab18eh2xzQtxUsHvWeY+1iFgyMluUuNR6taoSmFAjEA31m2czguZhKYX+4JSKu5pRYhBTXAd8KKQ3xdPRX/qCaLvT2qJAEQ1YQM3EJRrtI7\"\n          }\n        ]\n      },\n      \"validFor\": {\n        \"start\": \"2025-04-09T00:00:00Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json",
    "content": "{\n \"signatures\": [\n  {\n   \"keyid\": \"e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2\",\n   \"sig\": \"3046022100e04c9706299be5d8c2b14fb50bcd5b9c241f10597153dfe22f943efe896b5150022100cfd7b9f06a5900784e312d02b8e336edbb3b2fab61ac14550b3112b4f9e33df4\"\n  },\n  {\n   \"keyid\": \"22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06\",\n   \"sig\": \"\"\n  },\n  {\n   \"keyid\": \"61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222\",\n   \"sig\": \"3045022100cc308ae7d390fa782ee3376ddfaa929835016e86dad81f69e2de7ec1e174432e02205fb19906a31cce146c29624443c0d0c2f33ee80dac39d72114f939607cc22937\"\n  },\n  {\n   \"keyid\": \"a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70\",\n   \"sig\": \"304502203f8aff7a30e05a8c3d904b671ab1a6e4e8a6f508b7cfa0c780e72976bee7a227022100f64c9b765526f34d9ea16339cf238893e1c3368b4f0910a61a1af27dda01ebb9\"\n  },\n  {\n   \"keyid\": \"183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632\",\n   \"sig\": \"304502202363ca249aefa6d5f61c408a32cdd079b034a7888ddf2136dc4515ed4a728418022100b04eca42bc510ccbbf5d30783aaa936b1f137ca7a017ee9d90d3710432da0427\"\n  }\n ],\n \"signed\": {\n  \"_type\": \"root\",\n  \"consistent_snapshot\": true,\n  \"expires\": \"2026-06-22T13:27:01Z\",\n  \"keys\": {\n   \"0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5\": {\n    \"keyid_hash_algorithms\": [\n     \"sha256\",\n     \"sha512\"\n    ],\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-online-uri\": \"gcpkms:projects/sigstore-root-signing/locations/global/keyRings/root/cryptoKeys/timestamp/cryptoKeyVersions/1\"\n   },\n   \"183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632\": {\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMxpPOJCIZ5otG4106fGJseEQi3V9\\npkMYQ4uyV9Tj1M7WHXIyLG+jkfvuG0glQ1JZbRZZBV3gAR4sojdGHISeow==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@lance\"\n   },\n   \"22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06\": {\n    \"keyid_hash_algorithms\": [\n     \"sha256\",\n     \"sha512\"\n    ],\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@santiagotorres\"\n   },\n   \"61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222\": {\n    \"keyid_hash_algorithms\": [\n     \"sha256\",\n     \"sha512\"\n    ],\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@bobcallaway\"\n   },\n   \"a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70\": {\n    \"keyid_hash_algorithms\": [\n     \"sha256\",\n     \"sha512\"\n    ],\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@joshuagl\"\n   },\n   \"e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2\": {\n    \"keyid_hash_algorithms\": [\n     \"sha256\",\n     \"sha512\"\n    ],\n    \"keytype\": \"ecdsa\",\n    \"keyval\": {\n     \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\\n-----END PUBLIC KEY-----\\n\"\n    },\n    \"scheme\": \"ecdsa-sha2-nistp256\",\n    \"x-tuf-on-ci-keyowner\": \"@mnm678\"\n   }\n  },\n  \"roles\": {\n   \"root\": {\n    \"keyids\": [\n     \"e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2\",\n     \"22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06\",\n     \"61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222\",\n     \"a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70\",\n     \"183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632\"\n    ],\n    \"threshold\": 3\n   },\n   \"snapshot\": {\n    \"keyids\": [\n     \"0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5\"\n    ],\n    \"threshold\": 1,\n    \"x-tuf-on-ci-expiry-period\": 3650,\n    \"x-tuf-on-ci-signing-period\": 365\n   },\n   \"targets\": {\n    \"keyids\": [\n     \"e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2\",\n     \"22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06\",\n     \"61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222\",\n     \"a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70\",\n     \"183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632\"\n    ],\n    \"threshold\": 3\n   },\n   \"timestamp\": {\n    \"keyids\": [\n     \"0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5\"\n    ],\n    \"threshold\": 1,\n    \"x-tuf-on-ci-expiry-period\": 7,\n    \"x-tuf-on-ci-signing-period\": 6\n   }\n  },\n  \"spec_version\": \"1.0\",\n  \"version\": 14,\n  \"x-tuf-on-ci-expiry-period\": 197,\n  \"x-tuf-on-ci-signing-period\": 46\n }\n}"
  },
  {
    "path": "sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/signing_config.v0.2.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n  \"caUrls\": [\n    {\n      \"url\": \"https://fulcio.sigstore.dev\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2022-04-13T20:06:15.000Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"oidcUrls\": [\n    {\n      \"url\": \"https://oauth2.sigstore.dev/auth\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2022-04-13T20:06:15.000Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"rekorTlogUrls\": [\n    {\n      \"url\": \"https://rekor.sigstore.dev\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2021-01-12T11:53:27.000Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"tsaUrls\": [\n    {\n      \"url\": \"https://timestamp.sigstore.dev/api/v1/timestamp\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-07-04T00:00:00Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"rekorTlogConfig\": {\n    \"selector\": \"ANY\"\n  },\n  \"tsaConfig\": {\n    \"selector\": \"ANY\"\n  }\n}\n"
  },
  {
    "path": "sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n  \"tlogs\": [\n    {\n      \"baseUrl\": \"https://rekor.sigstore.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2021-01-12T11:53:27Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://log2025-1.rekor.sigstore.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=\",\n        \"keyDetails\": \"PKIX_ED25519\",\n        \"validFor\": {\n          \"start\": \"2025-09-23T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=\"\n      }\n    }\n  ],\n  \"certificateAuthorities\": [\n    {\n      \"subject\": {\n        \"organization\": \"sigstore.dev\",\n        \"commonName\": \"sigstore\"\n      },\n      \"uri\": \"https://fulcio.sigstore.dev\",\n      \"certChain\": {\n        \"certificates\": [\n          {\n            \"rawBytes\": \"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\"\n          }\n        ]\n      },\n      \"validFor\": {\n        \"start\": \"2021-03-07T03:20:29Z\",\n        \"end\": \"2022-12-31T23:59:59.999Z\"\n      }\n    },\n    {\n      \"subject\": {\n        \"organization\": \"sigstore.dev\",\n        \"commonName\": \"sigstore\"\n      },\n      \"uri\": \"https://fulcio.sigstore.dev\",\n      \"certChain\": {\n        \"certificates\": [\n          {\n            \"rawBytes\": \"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=\"\n          },\n          {\n            \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\"\n          }\n        ]\n      },\n      \"validFor\": {\n        \"start\": \"2022-04-13T20:06:15Z\"\n      }\n    }\n  ],\n  \"ctlogs\": [\n    {\n      \"baseUrl\": \"https://ctfe.sigstore.dev/test\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2021-03-14T00:00:00Z\",\n          \"end\": \"2022-10-31T23:59:59.999Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://ctfe.sigstore.dev/2022\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2022-10-20T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=\"\n      }\n    }\n  ],\n  \"timestampAuthorities\": [\n    {\n      \"subject\": {\n        \"organization\": \"sigstore.dev\",\n        \"commonName\": \"sigstore-tsa-selfsigned\"\n      },\n      \"uri\": \"https://timestamp.sigstore.dev/api/v1/timestamp\",\n      \"certChain\": {\n        \"certificates\": [\n          {\n            \"rawBytes\": \"MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw==\"\n          },\n          {\n            \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E\"\n          }\n        ]\n      },\n      \"validFor\": {\n        \"start\": \"2025-07-04T00:00:00Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "sigstore/_utils.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nShared utilities.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport sys\nfrom datetime import datetime, timezone\nfrom typing import IO, NewType, Union\nfrom urllib import parse\n\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa\nfrom cryptography.x509 import (\n    Certificate,\n    ExtensionNotFound,\n    Version,\n    load_der_x509_certificate,\n)\nfrom cryptography.x509.oid import ExtendedKeyUsageOID, ExtensionOID\nfrom sigstore_models.common.v1 import HashAlgorithm, TimeRange\n\nfrom sigstore import hashes as sigstore_hashes\nfrom sigstore.errors import VerificationError\n\nif sys.version_info < (3, 11):\n    import importlib_resources as resources\nelse:\n    from importlib import resources\n\n\nPublicKey = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey]\n\nPublicKeyTypes = Union[\n    type[rsa.RSAPublicKey],\n    type[ec.EllipticCurvePublicKey],\n    type[ed25519.Ed25519PublicKey],\n]\n\nHexStr = NewType(\"HexStr\", str)\n\"\"\"\nA newtype for `str` objects that contain hexadecimal strings (e.g. `ffabcd00ff`).\n\"\"\"\nB64Str = NewType(\"B64Str\", str)\n\"\"\"\nA newtype for `str` objects that contain base64 encoded strings.\n\"\"\"\nKeyID = NewType(\"KeyID\", bytes)\n\"\"\"\nA newtype for `bytes` objects that contain a key id.\n\"\"\"\n\n\ndef load_pem_public_key(\n    key_pem: bytes,\n    *,\n    types: tuple[PublicKeyTypes, ...] = (\n        rsa.RSAPublicKey,\n        ec.EllipticCurvePublicKey,\n        ed25519.Ed25519PublicKey,\n    ),\n) -> PublicKey:\n    \"\"\"\n    A specialization of `cryptography`'s `serialization.load_pem_public_key`\n    with a uniform exception type (`VerificationError`) and filtering on valid key types\n    for Sigstore purposes.\n    \"\"\"\n\n    try:\n        key = serialization.load_pem_public_key(key_pem)\n    except Exception as exc:\n        raise VerificationError(\"could not load PEM-formatted public key\") from exc\n\n    if not isinstance(key, types):\n        raise VerificationError(f\"invalid key format: not one of {types}\")\n\n    return key\n\n\ndef load_der_public_key(\n    key_der: bytes,\n    *,\n    types: tuple[PublicKeyTypes, ...] = (\n        rsa.RSAPublicKey,\n        ec.EllipticCurvePublicKey,\n        ed25519.Ed25519PublicKey,\n    ),\n) -> PublicKey:\n    \"\"\"\n    The `load_pem_public_key` specialization, but DER.\n    \"\"\"\n\n    try:\n        key = serialization.load_der_public_key(key_der)\n    except Exception as exc:\n        raise VerificationError(\"could not load DER-formatted public key\") from exc\n\n    if not isinstance(key, types):\n        raise VerificationError(f\"invalid key format: not one of {types}\")\n\n    return key\n\n\ndef base64_encode_pem_cert(cert: Certificate) -> B64Str:\n    \"\"\"\n    Returns a string containing a base64-encoded PEM-encoded X.509 certificate.\n    \"\"\"\n\n    return B64Str(\n        base64.b64encode(cert.public_bytes(serialization.Encoding.PEM)).decode()\n    )\n\n\ndef cert_der_to_pem(der: bytes) -> str:\n    \"\"\"\n    Converts a DER-encoded X.509 certificate into its PEM encoding.\n\n    Returns a string containing a PEM-encoded X.509 certificate.\n    \"\"\"\n\n    # NOTE: Technically we don't have to round-trip like this, since\n    # the DER-to-PEM transformation is entirely mechanical.\n    cert = load_der_x509_certificate(der)\n    return cert.public_bytes(serialization.Encoding.PEM).decode()\n\n\ndef key_id(key: PublicKey) -> KeyID:\n    \"\"\"\n    Returns an RFC 6962-style \"key ID\" for the given public key.\n\n    See: <https://www.rfc-editor.org/rfc/rfc6962#section-3.2>\n    \"\"\"\n    public_bytes = key.public_bytes(\n        encoding=serialization.Encoding.DER,\n        format=serialization.PublicFormat.SubjectPublicKeyInfo,\n    )\n\n    return KeyID(hashlib.sha256(public_bytes).digest())\n\n\ndef sha256_digest(\n    input_: bytes | IO[bytes] | sigstore_hashes.Hashed,\n) -> sigstore_hashes.Hashed:\n    \"\"\"\n    Compute the SHA256 digest of an input stream or buffer or,\n    if given a `Hashed`, return it directly.\n    \"\"\"\n    if isinstance(input_, sigstore_hashes.Hashed):\n        return input_\n\n    # If the input is already buffered into memory, there's no point in\n    # going back through an I/O abstraction.\n    if isinstance(input_, bytes):\n        return sigstore_hashes.Hashed(\n            digest=hashlib.sha256(input_).digest(), algorithm=HashAlgorithm.SHA2_256\n        )\n\n    return sigstore_hashes.Hashed(\n        digest=_sha256_streaming(input_), algorithm=HashAlgorithm.SHA2_256\n    )\n\n\ndef _sha256_streaming(io: IO[bytes]) -> bytes:\n    \"\"\"\n    Compute the SHA256 of a stream.\n\n    This function does its own internal buffering, so an unbuffered stream\n    should be supplied for optimal performance.\n    \"\"\"\n\n    # NOTE: This function performs a SHA256 digest over a stream.\n    # The stream's size is not checked, meaning that the stream's source\n    # is implicitly trusted: if an attacker is able to truncate the stream's\n    # source prematurely, then they could conceivably produce a digest\n    # for a partial stream. This in turn could conceivably result\n    # in a valid signature for an unintended (truncated) input.\n    #\n    # This is currently outside of sigstore-python's threat model: we\n    # assume that the stream is trusted.\n    #\n    # See: https://github.com/sigstore/sigstore-python/pull/329#discussion_r1041215972\n\n    sha256 = hashlib.sha256()\n    # Per coreutils' ioblksize.h: 128KB performs optimally across a range\n    # of systems in terms of minimizing syscall overhead.\n    view = memoryview(bytearray(128 * 1024))\n\n    nbytes = io.readinto(view)  # type: ignore[attr-defined]\n    while nbytes:\n        sha256.update(view[:nbytes])\n        nbytes = io.readinto(view)  # type: ignore[attr-defined]\n\n    return sha256.digest()\n\n\ndef read_embedded(name: str, url: str) -> bytes:\n    \"\"\"\n    Read a resource for a given TUF repository embedded in this distribution\n    of sigstore-python, returning its contents as bytes.\n    \"\"\"\n    embed_dir = parse.quote(url, safe=\"\")\n    b: bytes = resources.files(\"sigstore._store\").joinpath(embed_dir, name).read_bytes()\n    return b\n\n\ndef cert_is_ca(cert: Certificate) -> bool:\n    \"\"\"\n    Returns `True` if and only if the given `Certificate`\n    is a CA certificate.\n\n    This function doesn't indicate the trustworthiness of the given\n    `Certificate`, only whether it has the appropriate interior state.\n\n    This function is **not** naively invertible: users **must** use the\n    dedicated `cert_is_leaf` utility function to determine whether a particular\n    leaf upholds Sigstore's invariants.\n    \"\"\"\n\n    # Only v3 certificates should appear in the context of Sigstore;\n    # earlier versions of X.509 lack extensions and have ambiguous CA\n    # behavior.\n    if cert.version != Version.v3:\n        raise VerificationError(f\"invalid X.509 version: {cert.version}\")\n\n    # Valid CA certificates must have the following set:\n    #\n    #  * `BasicKeyUsage.keyCertSign`\n    #  * `BasicConstraints.ca`\n    #\n    # Any other combination of states is inconsistent and invalid, meaning\n    # that we won't consider the certificate a valid non-CA leaf.\n\n    try:\n        basic_constraints = cert.extensions.get_extension_for_oid(\n            ExtensionOID.BASIC_CONSTRAINTS\n        )\n\n        # BasicConstraints must be marked as critical, per RFC 5280 4.2.1.9.\n        if not basic_constraints.critical:\n            raise VerificationError(\n                \"invalid X.509 certificate: non-critical BasicConstraints in CA\"\n            )\n\n        ca = basic_constraints.value.ca  # type: ignore[attr-defined]\n    except ExtensionNotFound:\n        # No BasicConstrains means that this can't possibly be a CA.\n        return False\n\n    key_cert_sign = False\n    try:\n        key_usage = cert.extensions.get_extension_for_oid(ExtensionOID.KEY_USAGE)\n        key_cert_sign = key_usage.value.key_cert_sign  # type: ignore[attr-defined]\n    except ExtensionNotFound:\n        raise VerificationError(\"invalid X.509 certificate: missing KeyUsage\")\n\n    # If both states are set, this is a CA.\n    if ca and key_cert_sign:\n        return True\n\n    if not (ca or key_cert_sign):\n        return False\n\n    # Anything else is an invalid state that should never occur.\n    raise VerificationError(\n        f\"invalid X.509 certificate states: KeyUsage.keyCertSign={key_cert_sign}\"\n        f\", BasicConstraints.ca={ca}\"\n    )\n\n\ndef cert_is_root_ca(cert: Certificate) -> bool:\n    \"\"\"\n    Returns `True` if and only if the given `Certificate` indicates\n    that it's a root CA.\n\n    This is **not** a verification function, and it does not establish\n    the trustworthiness of the given certificate.\n    \"\"\"\n\n    # NOTE(ww): This function is obnoxiously long to make the different\n    # states explicit.\n\n    # Only v3 certificates should appear in the context of Sigstore;\n    # earlier versions of X.509 lack extensions and have ambiguous CA\n    # behavior.\n    if cert.version != Version.v3:\n        raise VerificationError(f\"invalid X.509 version: {cert.version}\")\n\n    # Non-CAs can't possibly be root CAs.\n    if not cert_is_ca(cert):\n        return False\n\n    # A certificate that is its own issuer and signer is considered a root CA.\n    try:\n        cert.verify_directly_issued_by(cert)\n        return True\n    except Exception:\n        return False\n\n\ndef cert_is_leaf(cert: Certificate) -> bool:\n    \"\"\"\n    Returns `True` if and only if the given `Certificate` is a valid\n    leaf certificate for Sigstore purposes. This means that:\n\n    * It is not a root or intermediate CA;\n    * It has `KeyUsage.digitalSignature`;\n    * It has `CODE_SIGNING` as an `ExtendedKeyUsage`.\n\n    This is **not** a verification function, and it does not establish\n    the trustworthiness of the given certificate.\n    \"\"\"\n\n    # Only v3 certificates should appear in the context of Sigstore;\n    # earlier versions of X.509 lack extensions and have ambiguous CA\n    # behavior.\n    if cert.version != Version.v3:\n        raise VerificationError(f\"invalid X.509 version: {cert.version}\")\n\n    # CAs are not leaves.\n    if cert_is_ca(cert):\n        return False\n\n    key_usage = cert.extensions.get_extension_for_oid(ExtensionOID.KEY_USAGE)\n    digital_signature = key_usage.value.digital_signature  # type: ignore[attr-defined]\n\n    if not digital_signature:\n        raise VerificationError(\n            \"invalid certificate for Sigstore purposes: missing digital signature usage\"\n        )\n\n    # Finally, we check to make sure the leaf has an `ExtendedKeyUsages`\n    # extension that includes a codesigning entitlement. Sigstore should\n    # never issue a leaf that doesn't have this extended usage.\n    try:\n        extended_key_usage = cert.extensions.get_extension_for_oid(\n            ExtensionOID.EXTENDED_KEY_USAGE\n        )\n\n        return ExtendedKeyUsageOID.CODE_SIGNING in extended_key_usage.value  # type: ignore[operator]\n    except ExtensionNotFound:\n        raise VerificationError(\"invalid X.509 certificate: missing ExtendedKeyUsage\")\n\n\ndef is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool:\n    \"\"\"\n    Given a `period`, checks that the the current time is not before `start`. If\n    `allow_expired` is `False`, also checks that the current time is not after\n    `end`.\n    \"\"\"\n    now = datetime.now(timezone.utc)\n\n    # If there was no validity period specified, the key is always valid.\n    if not period:\n        return True\n\n    # Active: if the current time is before the starting period, we are not yet\n    # valid.\n    if now < period.start:\n        return False\n\n    # If we want Expired keys, the key is valid at this point. Otherwise, check\n    # that we are within range.\n    return allow_expired or (period.end is None or now <= period.end)\n"
  },
  {
    "path": "sigstore/dsse/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nFunctionality for building and manipulating in-toto Statements and DSSE envelopes.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport logging\nfrom typing import Any, Literal, Optional\n\nfrom cryptography.exceptions import InvalidSignature\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError\nfrom sigstore_models.common.v1 import HashAlgorithm\nfrom sigstore_models.intoto import Envelope as _Envelope\nfrom sigstore_models.intoto import Signature as _Signature\n\nfrom sigstore.errors import Error, VerificationError\nfrom sigstore.hashes import Hashed\n\n_logger = logging.getLogger(__name__)\n\nDigest = Literal[\"sha256\", \"sha384\", \"sha512\", \"sha3_256\", \"sha3_384\", \"sha3_512\"]\n\"\"\"\nNOTE: in-toto's DigestSet contains all kinds of hash algorithms that\nwe intentionally do not support. This model is limited to common members of the\nSHA-2 and SHA-3 family that are at least as strong as SHA-256.\n\nSee: <https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md>\n\"\"\"\n\nDigestSet = RootModel[dict[Digest, str]]\n\"\"\"\nAn internal validation model for in-toto subject digest sets.\n\"\"\"\n\n\nclass Subject(BaseModel):\n    \"\"\"\n    A single in-toto statement subject.\n    \"\"\"\n\n    name: Optional[StrictStr]  # noqa: UP045\n    digest: DigestSet = Field(...)\n\n\nclass _Statement(BaseModel):\n    \"\"\"\n    An internal validation model for in-toto statements.\n    \"\"\"\n\n    model_config = ConfigDict(populate_by_name=True)\n\n    type_: Literal[\"https://in-toto.io/Statement/v1\"] = Field(..., alias=\"_type\")\n    subjects: list[Subject] = Field(..., min_length=1, alias=\"subject\")\n    predicate_type: StrictStr = Field(..., alias=\"predicateType\")\n    predicate: Optional[dict[str, Any]] = Field(None, alias=\"predicate\")  # noqa: UP045\n\n\nclass Statement:\n    \"\"\"\n    Represents an in-toto statement.\n\n    This type deals with opaque bytes to ensure that the encoding does not\n    change, but Statements are internally checked for conformance against\n    the JSON object layout defined in the in-toto attestation spec.\n\n    See: <https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md>\n    \"\"\"\n\n    def __init__(self, contents: bytes | _Statement) -> None:\n        \"\"\"\n        Construct a new Statement.\n\n        This takes an opaque `bytes` containing the statement; use\n        `StatementBuilder` to manually construct an in-toto statement\n        from constituent pieces.\n        \"\"\"\n        if isinstance(contents, bytes):\n            self._contents = contents\n            try:\n                self._inner = _Statement.model_validate_json(contents)\n            except ValidationError:\n                raise Error(\"malformed in-toto statement\")\n        else:\n            self._contents = contents.model_dump_json(by_alias=True).encode()\n            self._inner = contents\n\n    def _matches_digest(self, digest: Hashed) -> bool:\n        \"\"\"\n        Returns a boolean indicating whether this in-toto Statement contains a subject\n        matching the given digest. The subject's name is **not** checked.\n\n        No digests other than SHA256 are currently supported.\n        \"\"\"\n        if digest.algorithm != HashAlgorithm.SHA2_256:\n            raise VerificationError(f\"unexpected digest algorithm: {digest.algorithm}\")\n\n        for sub in self._inner.subjects:\n            sub_digest = sub.digest.root.get(\"sha256\")\n            if sub_digest is None:\n                continue\n            if sub_digest == digest.digest.hex():\n                return True\n\n        return False\n\n    def _pae(self) -> bytes:\n        \"\"\"\n        Construct the PAE encoding for this statement.\n        \"\"\"\n\n        return _pae(Envelope._TYPE, self._contents)\n\n\nclass StatementBuilder:\n    \"\"\"\n    A builder-style API for constructing in-toto Statements.\n    \"\"\"\n\n    def __init__(\n        self,\n        subjects: list[Subject] | None = None,\n        predicate_type: str | None = None,\n        predicate: dict[str, Any] | None = None,\n    ):\n        \"\"\"\n        Create a new `StatementBuilder`.\n        \"\"\"\n        self._subjects = subjects or []\n        self._predicate_type = predicate_type\n        self._predicate = predicate\n\n    def subjects(self, subjects: list[Subject]) -> StatementBuilder:\n        \"\"\"\n        Configure the subjects for this builder.\n        \"\"\"\n        self._subjects = subjects\n        return self\n\n    def predicate_type(self, predicate_type: str) -> StatementBuilder:\n        \"\"\"\n        Configure the predicate type for this builder.\n        \"\"\"\n        self._predicate_type = predicate_type\n        return self\n\n    def predicate(self, predicate: dict[str, Any]) -> StatementBuilder:\n        \"\"\"\n        Configure the predicate for this builder.\n        \"\"\"\n        self._predicate = predicate\n        return self\n\n    def build(self) -> Statement:\n        \"\"\"\n        Build a `Statement` from the builder's state.\n        \"\"\"\n        try:\n            stmt = _Statement(\n                type_=\"https://in-toto.io/Statement/v1\",\n                subjects=self._subjects,\n                predicate_type=self._predicate_type,\n                predicate=self._predicate,\n            )\n        except ValidationError as e:\n            raise Error(f\"invalid statement: {e}\")\n\n        return Statement(stmt)\n\n\nclass InvalidEnvelope(Error):\n    \"\"\"\n    Raised when the associated `Envelope` is invalid in some way.\n    \"\"\"\n\n\nclass Envelope:\n    \"\"\"\n    Represents a DSSE envelope.\n\n    This class cannot be constructed directly; you must use `sign` or `from_json`.\n\n    See: <https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md>\n    \"\"\"\n\n    _TYPE = \"application/vnd.in-toto+json\"\n\n    def __init__(self, inner: _Envelope) -> None:\n        \"\"\"\n        @private\n        \"\"\"\n\n        self._inner = inner\n        self._verify()\n\n    def _verify(self) -> None:\n        \"\"\"\n        Verify and load the Envelope.\n        \"\"\"\n        if len(self._inner.signatures) != 1:\n            raise InvalidEnvelope(\"envelope must contain exactly one signature\")\n\n        if not self._inner.signatures[0].sig:\n            raise InvalidEnvelope(\"envelope signature must be non-empty\")\n\n        self._signature_bytes = self._inner.signatures[0].sig\n\n    @classmethod\n    def _from_json(cls, contents: bytes | str) -> Envelope:\n        \"\"\"Return a DSSE envelope from the given JSON representation.\"\"\"\n        inner = _Envelope.from_json(contents)\n        return cls(inner)\n\n    def to_json(self) -> str:\n        \"\"\"\n        Return a JSON string with this DSSE envelope's contents.\n        \"\"\"\n        return self._inner.to_json()\n\n    def __eq__(self, other: object) -> bool:\n        \"\"\"Equality for DSSE envelopes.\"\"\"\n\n        if not isinstance(other, Envelope):\n            return NotImplemented\n\n        return self._inner == other._inner\n\n    @property\n    def signature(self) -> bytes:\n        \"\"\"Return the decoded bytes of the Envelope signature.\"\"\"\n        return self._signature_bytes\n\n\ndef _pae(type_: str, body: bytes) -> bytes:\n    \"\"\"\n    Compute the PAE encoding for the given `type_` and `body`.\n    \"\"\"\n\n    # See:\n    # https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md\n    # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md\n    pae = f\"DSSEv1 {len(type_)} {type_} \".encode()\n    pae += b\" \".join([str(len(body)).encode(), body])\n    return pae\n\n\ndef _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope:\n    \"\"\"\n    Sign for the given in-toto `Statement`, and encapsulate the resulting\n    signature in a DSSE `Envelope`.\n    \"\"\"\n    pae = stmt._pae()\n    _logger.debug(f\"DSSE PAE: {pae!r}\")\n\n    signature = key.sign(pae, ec.ECDSA(hashes.SHA256()))\n    return Envelope(\n        _Envelope(\n            payload=base64.b64encode(stmt._contents),\n            payload_type=Envelope._TYPE,\n            signatures=[_Signature(sig=base64.b64encode(signature))],\n        )\n    )\n\n\ndef _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes:\n    \"\"\"\n    Verify the given in-toto `Envelope`, returning the verified inner payload.\n\n    This function does **not** check the envelope's payload type. The caller\n    is responsible for performing this check.\n    \"\"\"\n\n    pae = _pae(evp._inner.payload_type, evp._inner.payload)\n\n    nsigs = len(evp._inner.signatures)\n    if nsigs != 1:\n        raise VerificationError(f\"DSSE: exactly 1 signature allowed, got {nsigs}\")\n\n    signature = evp._inner.signatures[0].sig\n\n    try:\n        key.verify(signature, pae, ec.ECDSA(hashes.SHA256()))\n    except InvalidSignature:\n        raise VerificationError(\"DSSE: invalid signature\")\n\n    return evp._inner.payload\n"
  },
  {
    "path": "sigstore/dsse/_predicate.py",
    "content": "# Copyright 2024 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nModels for the predicates used in in-toto statements\n\"\"\"\n\nimport enum\nfrom typing import Any, Literal, Union\n\nfrom pydantic import (\n    BaseModel,\n    ConfigDict,\n    RootModel,\n    StrictBytes,\n    StrictStr,\n    model_validator,\n)\nfrom pydantic.alias_generators import to_camel\nfrom typing_extensions import Self\n\nfrom sigstore.dsse import Digest\n\n\nclass PredicateType(str, enum.Enum):\n    \"\"\"\n    Currently supported predicate types\n    \"\"\"\n\n    SLSA_v0_2 = \"https://slsa.dev/provenance/v0.2\"\n    SLSA_v1_0 = \"https://slsa.dev/provenance/v1\"\n\n\n# Common models\nSourceDigest = Literal[\"sha1\", \"gitCommit\"]\nDigestSetSource = RootModel[dict[Union[Digest, SourceDigest], str]]\n\"\"\"\nSame as `dsse.DigestSet` but with `sha1` added.\n\nSince this model is not used to verify hashes, but to parse predicates that might\ncontain hashes, we include this weak hash algorithm. This is because provenance\nproviders like GitHub use SHA1 in their predicates to refer to git commit hashes.\n\"\"\"\n\n\nclass Predicate(BaseModel):\n    \"\"\"\n    Base model for in-toto predicates\n    \"\"\"\n\n    pass\n\n\nclass _SLSAConfigBase(BaseModel):\n    \"\"\"\n    Base class used to configure the models\n    \"\"\"\n\n    model_config = ConfigDict(alias_generator=to_camel, extra=\"forbid\")\n\n\n# Models for SLSA Provenance v0.2\n\n\nclass BuilderV0_1(_SLSAConfigBase):\n    \"\"\"\n    The Builder object used by SLSAPredicateV0_2\n    \"\"\"\n\n    id: StrictStr\n\n\nclass ConfigSource(_SLSAConfigBase):\n    \"\"\"\n    The ConfigSource object used by Invocation in v0.2\n    \"\"\"\n\n    uri: StrictStr | None = None\n    digest: DigestSetSource | None = None\n    entry_point: StrictStr | None = None\n\n\nclass Invocation(_SLSAConfigBase):\n    \"\"\"\n    The Invocation object used by SLSAPredicateV0_2\n    \"\"\"\n\n    config_source: ConfigSource | None = None\n    parameters: dict[str, Any] | None = None\n    environment: dict[str, Any] | None = None\n\n\nclass Completeness(_SLSAConfigBase):\n    \"\"\"\n    The Completeness object used by Metadata in v0.2\n    \"\"\"\n\n    parameters: bool | None = None\n    environment: bool | None = None\n    materials: bool | None = None\n\n\nclass Material(_SLSAConfigBase):\n    \"\"\"\n    The Material object used by Metadata in v0.2\n    \"\"\"\n\n    uri: StrictStr | None = None\n    digest: DigestSetSource | None = None\n\n\nclass Metadata(_SLSAConfigBase):\n    \"\"\"\n    The Metadata object used by SLSAPredicateV0_2\n    \"\"\"\n\n    build_invocation_id: StrictStr | None = None\n    build_started_on: StrictStr | None = None\n    build_finished_on: StrictStr | None = None\n    completeness: Completeness | None = None\n    reproducible: bool | None = None\n\n\nclass SLSAPredicateV0_2(Predicate, _SLSAConfigBase):\n    \"\"\"\n    Represents the predicate object corresponding to the type \"https://slsa.dev/provenance/v0.2\"\n    \"\"\"\n\n    builder: BuilderV0_1\n    build_type: StrictStr\n    invocation: Invocation | None = None\n    metadata: Metadata | None = None\n    build_config: dict[str, Any] | None = None\n    materials: list[Material] | None = None\n\n\n# Models for SLSA Provenance v1.0\n\n\nclass ResourceDescriptor(_SLSAConfigBase):\n    \"\"\"\n    The ResourceDescriptor object defined defined by the in-toto attestations spec\n    \"\"\"\n\n    name: StrictStr | None = None\n    uri: StrictStr | None = None\n    digest: DigestSetSource | None = None\n    content: StrictBytes | None = None\n    download_location: StrictStr | None = None\n    media_type: StrictStr | None = None\n    annotations: dict[StrictStr, Any] | None = None\n\n    @model_validator(mode=\"after\")\n    def check_required_fields(self: Self) -> Self:\n        \"\"\"\n        While all fields are optional, at least one of the fields `uri`, `digest` or\n        `content` must be present\n        \"\"\"\n        if not self.uri and not self.digest and not self.content:\n            raise ValueError(\n                \"A ResourceDescriptor MUST specify one of uri, digest or content at a minimum\"\n            )\n        return self\n\n\nclass BuilderV1_0(_SLSAConfigBase):\n    \"\"\"\n    The Builder object used by RunDetails in v1.0\n    \"\"\"\n\n    id: StrictStr\n    builder_dependencies: list[ResourceDescriptor] | None = None\n    version: dict[StrictStr, StrictStr] | None = None\n\n\nclass BuildMetadata(_SLSAConfigBase):\n    \"\"\"\n    The BuildMetadata object used by RunDetails\n    \"\"\"\n\n    invocation_id: StrictStr | None = None\n    started_on: StrictStr | None = None\n    finished_on: StrictStr | None = None\n\n\nclass RunDetails(_SLSAConfigBase):\n    \"\"\"\n    The RunDetails object used by SLSAPredicateV1_0\n    \"\"\"\n\n    builder: BuilderV1_0\n    metadata: BuildMetadata | None = None\n    byproducts: list[ResourceDescriptor] | None = None\n\n\nclass BuildDefinition(_SLSAConfigBase):\n    \"\"\"\n    The BuildDefinition object used by SLSAPredicateV1_0\n    \"\"\"\n\n    build_type: StrictStr\n    external_parameters: dict[StrictStr, Any]\n    internal_parameters: dict[str, Any] | None = None\n    resolved_dependencies: list[ResourceDescriptor] | None = None\n\n\nclass SLSAPredicateV1_0(Predicate, _SLSAConfigBase):\n    \"\"\"\n    Represents the predicate object corresponding to the type \"https://slsa.dev/provenance/v1\"\n    \"\"\"\n\n    build_definition: BuildDefinition\n    run_details: RunDetails\n"
  },
  {
    "path": "sigstore/errors.py",
    "content": "# Copyright 2023 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nExceptions.\n\"\"\"\n\nimport sys\nfrom collections.abc import Mapping\nfrom logging import Logger\nfrom typing import Any, NoReturn\n\n\nclass Error(Exception):\n    \"\"\"Base sigstore exception type. Defines helpers for diagnostics.\"\"\"\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns human-friendly error information.\"\"\"\n\n        return str(self)\n\n    def log_and_exit(self, logger: Logger, raise_error: bool = False) -> NoReturn:\n        \"\"\"Prints all relevant error information to stderr and exits.\"\"\"\n\n        remind_verbose = (\n            \"Raising original exception:\"\n            if raise_error\n            else \"For detailed error information, run sigstore with the `--verbose` flag.\"\n        )\n\n        logger.error(f\"{self.diagnostics()}\\n{remind_verbose}\")\n\n        if raise_error:\n            # don't want \"during handling another exception\"\n            self.__suppress_context__ = True\n            raise self\n\n        sys.exit(1)\n\n\nclass NetworkError(Error):\n    \"\"\"Raised when a connectivity-related issue occurs.\"\"\"\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns diagnostics for the error.\"\"\"\n\n        cause_ctx = (\n            f\"\"\"\n        Additional context:\n\n        {self.__cause__}\n        \"\"\"\n            if self.__cause__\n            else \"\"\n        )\n\n        return (\n            \"\"\"\\\n        A network issue occurred.\n\n        Check your internet connection and try again.\n        \"\"\"\n            + cause_ctx\n        )\n\n\nclass TUFError(Error):\n    \"\"\"Raised when a TUF error occurs.\"\"\"\n\n    def __init__(self, message: str):\n        \"\"\"Constructs a `TUFError`.\"\"\"\n        self.message = message\n\n    from tuf.api import exceptions\n\n    _details: Mapping[Any, str] = {\n        exceptions.DownloadError: NetworkError().diagnostics()\n    }\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns diagnostics specialized to the wrapped TUF error.\"\"\"\n        details = TUFError._details.get(\n            type(self.__context__),\n            \"Please check any Sigstore instance related arguments and consider \"\n            \"reporting the issue at <https://github.com/sigstore/sigstore-python/issues/new>.\",\n        )\n\n        return f\"\"\"\\\n        {self.message}.\n\n        {details}\n        \"\"\"\n\n\nclass MetadataError(Error):\n    \"\"\"Raised when TUF metadata does not conform to the expected structure.\"\"\"\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns diagnostics for the error.\"\"\"\n        return f\"\"\"{self}.\"\"\"\n\n\nclass RootError(Error):\n    \"\"\"Raised when TUF cannot establish its root of trust.\"\"\"\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns diagnostics for the error.\"\"\"\n        return \"\"\"\\\n        Unable to establish root of trust.\n\n        This error may occur when the resources embedded in this distribution of sigstore-python are out of date.\"\"\"\n\n\nclass VerificationError(Error):\n    \"\"\"\n    Raised whenever any phase or subcomponent of Sigstore verification fails.\n    \"\"\"\n\n\nclass CertValidationError(VerificationError):\n    \"\"\"\n    Raised when a TSA certificate chain fails to validate during Sigstore verification.\n\n    This is used by CLI to hint that an incorrect Sigstore instance may have been used\n    \"\"\"\n"
  },
  {
    "path": "sigstore/hashes.py",
    "content": "# Copyright 2023 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nHashing APIs.\n\"\"\"\n\nimport rekor_types\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric.utils import Prehashed\nfrom pydantic import BaseModel\nfrom sigstore_models.common.v1 import HashAlgorithm\n\nfrom sigstore.errors import Error\n\n\nclass Hashed(BaseModel, frozen=True):\n    \"\"\"\n    Represents a hashed value.\n    \"\"\"\n\n    algorithm: HashAlgorithm\n    \"\"\"\n    The digest algorithm uses to compute the digest.\n    \"\"\"\n\n    digest: bytes\n    \"\"\"\n    The digest representing the hash value.\n    \"\"\"\n\n    def _as_hashedrekord_algorithm(self) -> rekor_types.hashedrekord.Algorithm:\n        \"\"\"\n        Returns an appropriate `hashedrekord.Algorithm` for this `Hashed`.\n        \"\"\"\n        if self.algorithm == HashAlgorithm.SHA2_256:\n            return rekor_types.hashedrekord.Algorithm.SHA256\n        raise Error(f\"unknown hash algorithm: {self.algorithm}\")\n\n    def _as_prehashed(self) -> Prehashed:\n        \"\"\"\n        Returns an appropriate Cryptography `Prehashed` for this `Hashed`.\n        \"\"\"\n        if self.algorithm == HashAlgorithm.SHA2_256:\n            return Prehashed(hashes.SHA256())\n        raise Error(f\"unknown hash algorithm: {self.algorithm}\")\n\n    def __str__(self) -> str:\n        \"\"\"\n        Returns a str representation of this `Hashed`.\n        \"\"\"\n        return f\"{self.algorithm.value}:{self.digest.hex()}\"\n"
  },
  {
    "path": "sigstore/models.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nCommon models shared between signing and verification.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport logging\nfrom collections import defaultdict\nfrom collections.abc import Iterable\nfrom enum import Enum\nfrom pathlib import Path\nfrom textwrap import dedent\nfrom typing import Any\n\nimport rfc8785\nfrom cryptography.hazmat.primitives.serialization import Encoding\nfrom cryptography.x509 import (\n    Certificate,\n    load_der_x509_certificate,\n)\nfrom pydantic import TypeAdapter\nfrom rekor_types import Dsse, Hashedrekord, ProposedEntry\nfrom rfc3161_client import TimeStampResponse, decode_timestamp_response\nfrom sigstore_models.bundle import v1 as bundle_v1\nfrom sigstore_models.bundle.v1 import Bundle as _Bundle\nfrom sigstore_models.bundle.v1 import (\n    TimestampVerificationData as _TimestampVerificationData,\n)\nfrom sigstore_models.bundle.v1 import VerificationMaterial as _VerificationMaterial\nfrom sigstore_models.common import v1 as common_v1\nfrom sigstore_models.common.v1 import MessageSignature, RFC3161SignedTimestamp\nfrom sigstore_models.rekor import v1 as rekor_v1\nfrom sigstore_models.rekor.v1 import TransparencyLogEntry as _TransparencyLogEntry\nfrom sigstore_models.trustroot import v1 as trustroot_v1\n\nfrom sigstore import dsse\nfrom sigstore._internal.fulcio.client import FulcioClient\nfrom sigstore._internal.merkle import verify_merkle_inclusion\nfrom sigstore._internal.rekor import RekorLogSubmitter\nfrom sigstore._internal.rekor.checkpoint import verify_checkpoint\nfrom sigstore._internal.timestamp import TimestampAuthorityClient\nfrom sigstore._internal.trust import (\n    CertificateAuthority,\n    CTKeyring,\n    Keyring,\n    KeyringPurpose,\n    RekorKeyring,\n)\nfrom sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater\nfrom sigstore._utils import KeyID, cert_is_leaf, cert_is_root_ca, is_timerange_valid\nfrom sigstore.errors import Error, MetadataError, TUFError, VerificationError\n\n# Versions supported by this client\nREKOR_VERSIONS = [1, 2]\nTSA_VERSIONS = [1]\nFULCIO_VERSIONS = [1]\nOIDC_VERSIONS = [1]\n\n\n_logger = logging.getLogger(__name__)\n\n\nclass TransparencyLogEntry:\n    \"\"\"\n    Represents a transparency log entry.\n    \"\"\"\n\n    def __init__(self, inner: _TransparencyLogEntry) -> None:\n        \"\"\"\n        Creates a new `TransparencyLogEntry` from the given inner object.\n\n        @private\n        \"\"\"\n        self._inner = inner\n        self._validate()\n\n    def _validate(self) -> None:\n        \"\"\"\n        Ensure this transparency log entry is well-formed and upholds our\n        client invariants.\n        \"\"\"\n\n        inclusion_proof: rekor_v1.InclusionProof | None = self._inner.inclusion_proof\n        # This check is required by us as the client, not the\n        # protobuf-specs themselves.\n        if not inclusion_proof or not inclusion_proof.checkpoint:\n            raise InvalidBundle(\"entry must contain inclusion proof, with checkpoint\")\n\n    def __eq__(self, value: object) -> bool:\n        \"\"\"\n        Compares this `TransparencyLogEntry` with another object for equality.\n\n        Two `TransparencyLogEntry` instances are considered equal if their\n        inner contents are equal.\n        \"\"\"\n        if not isinstance(value, TransparencyLogEntry):\n            return NotImplemented\n        return self._inner == value._inner\n\n    @classmethod\n    def _from_v1_response(cls, dict_: dict[str, Any]) -> TransparencyLogEntry:\n        \"\"\"\n        Create a new `TransparencyLogEntry` from the given API response.\n        \"\"\"\n\n        # Assumes we only get one entry back\n        entries = list(dict_.items())\n        if len(entries) != 1:\n            raise ValueError(\"Received multiple entries in response\")\n        _, entry = entries[0]\n\n        # Fill in the appropriate kind\n        body_entry: ProposedEntry = TypeAdapter(ProposedEntry).validate_json(\n            base64.b64decode(entry[\"body\"])\n        )\n        if not isinstance(body_entry, Hashedrekord | Dsse):\n            raise InvalidBundle(\"log entry is not of expected type\")\n\n        raw_inclusion_proof = entry[\"verification\"][\"inclusionProof\"]\n\n        # NOTE: The type ignores below are a consequence of our Pydantic\n        # modeling: mypy and other typecheckers see `ProtoU64` as `int`,\n        # but it gets coerced from a string due to Protobuf's JSON serialization.\n        inner = _TransparencyLogEntry(\n            log_index=str(entry[\"logIndex\"]),  # type: ignore[arg-type]\n            log_id=common_v1.LogId(\n                key_id=base64.b64encode(bytes.fromhex(entry[\"logID\"]))\n            ),\n            kind_version=rekor_v1.KindVersion(\n                kind=body_entry.kind, version=body_entry.api_version\n            ),\n            integrated_time=str(entry[\"integratedTime\"]),  # type: ignore[arg-type]\n            inclusion_promise=rekor_v1.InclusionPromise(\n                signed_entry_timestamp=entry[\"verification\"][\"signedEntryTimestamp\"]\n            ),\n            inclusion_proof=rekor_v1.InclusionProof(\n                log_index=str(raw_inclusion_proof[\"logIndex\"]),  # type: ignore[arg-type]\n                root_hash=base64.b64encode(\n                    bytes.fromhex(raw_inclusion_proof[\"rootHash\"])\n                ),\n                tree_size=str(raw_inclusion_proof[\"treeSize\"]),  # type: ignore[arg-type]\n                hashes=[\n                    base64.b64encode(bytes.fromhex(h))\n                    for h in raw_inclusion_proof[\"hashes\"]\n                ],\n                checkpoint=rekor_v1.Checkpoint(\n                    envelope=raw_inclusion_proof[\"checkpoint\"]\n                ),\n            ),\n            canonicalized_body=entry[\"body\"],\n        )\n\n        return cls(inner)\n\n    def _encode_canonical(self) -> bytes:\n        \"\"\"\n        Returns a canonicalized JSON (RFC 8785) representation of the transparency log entry.\n\n        This encoded representation is suitable for verification against\n        the Signed Entry Timestamp.\n        \"\"\"\n        # We might not have an integrated time if our log entry is from rekor\n        # v2, i.e. was integrated synchronously instead of via an\n        # inclusion promise.\n        if self._inner.integrated_time is None:\n            raise ValueError(\n                \"can't encode canonical form for SET without integrated time\"\n            )\n\n        payload: dict[str, int | str] = {\n            \"body\": base64.b64encode(self._inner.canonicalized_body).decode(),\n            \"integratedTime\": self._inner.integrated_time,\n            \"logID\": self._inner.log_id.key_id.hex(),\n            \"logIndex\": self._inner.log_index,\n        }\n\n        return rfc8785.dumps(payload)\n\n    def _verify_set(self, keyring: RekorKeyring) -> None:\n        \"\"\"\n        Verify the inclusion promise (Signed Entry Timestamp) for a given transparency log\n        `entry` using the given `keyring`.\n\n        Fails if the given log entry does not contain an inclusion promise.\n        \"\"\"\n\n        if self._inner.inclusion_promise is None:\n            raise VerificationError(\"SET: invalid inclusion promise: missing\")\n\n        signed_entry_ts = self._inner.inclusion_promise.signed_entry_timestamp\n\n        try:\n            keyring.verify(\n                key_id=KeyID(self._inner.log_id.key_id),\n                signature=signed_entry_ts,\n                data=self._encode_canonical(),\n            )\n        except VerificationError as exc:\n            raise VerificationError(f\"SET: invalid inclusion promise: {exc}\")\n\n    def _verify(self, keyring: RekorKeyring) -> None:\n        \"\"\"\n        Verifies this log entry.\n\n        This method performs steps (5), (6), and optionally (7) in\n        the top-level verify API:\n\n        * Verifies the consistency of the entry with the given bundle;\n        * Verifies the Merkle inclusion proof and its signed checkpoint;\n        * Verifies the inclusion promise, if present.\n        \"\"\"\n\n        verify_merkle_inclusion(self)\n        verify_checkpoint(keyring, self)\n\n        _logger.debug(\n            f\"successfully verified inclusion proof: index={self._inner.log_index}\"\n        )\n\n        if self._inner.inclusion_promise and self._inner.integrated_time:\n            self._verify_set(keyring)\n            _logger.debug(\n                f\"successfully verified inclusion promise: index={self._inner.log_index}\"\n            )\n\n\nclass TimestampVerificationData:\n    \"\"\"\n    Represents a TimestampVerificationData structure.\n\n    @private\n    \"\"\"\n\n    def __init__(self, inner: _TimestampVerificationData) -> None:\n        \"\"\"Init method.\"\"\"\n        self._inner = inner\n        self._verify()\n\n    def _verify(self) -> None:\n        \"\"\"\n        Verifies the TimestampVerificationData.\n\n        It verifies that TimeStamp Responses embedded in the bundle are correctly\n        formed.\n        \"\"\"\n        if not (timestamps := self._inner.rfc3161_timestamps):\n            timestamps = []\n\n        try:\n            self._signed_ts = [\n                decode_timestamp_response(ts.signed_timestamp) for ts in timestamps\n            ]\n        except ValueError:\n            raise VerificationError(\"Invalid Timestamp Response\")\n\n    @property\n    def rfc3161_timestamps(self) -> list[TimeStampResponse]:\n        \"\"\"Returns a list of signed timestamp.\"\"\"\n        return self._signed_ts\n\n    @classmethod\n    def from_json(cls, raw: str | bytes) -> TimestampVerificationData:\n        \"\"\"\n        Deserialize the given timestamp verification data.\n        \"\"\"\n        inner = _TimestampVerificationData.from_json(raw)\n        return cls(inner)\n\n\nclass VerificationMaterial:\n    \"\"\"\n    Represents a VerificationMaterial structure.\n    \"\"\"\n\n    def __init__(self, inner: _VerificationMaterial) -> None:\n        \"\"\"Init method.\"\"\"\n        self._inner = inner\n\n    @property\n    def timestamp_verification_data(self) -> TimestampVerificationData | None:\n        \"\"\"\n        Returns the Timestamp Verification Data, if present.\n        \"\"\"\n        if (\n            self._inner.timestamp_verification_data\n            and self._inner.timestamp_verification_data.rfc3161_timestamps\n        ):\n            return TimestampVerificationData(self._inner.timestamp_verification_data)\n        return None\n\n\nclass InvalidBundle(Error):\n    \"\"\"\n    Raised when the associated `Bundle` is invalid in some way.\n    \"\"\"\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns diagnostics for the error.\"\"\"\n\n        return dedent(\n            f\"\"\"\\\n        An issue occurred while parsing the Sigstore bundle.\n\n        The provided bundle is malformed and may have been modified maliciously.\n\n        Additional context:\n\n        {self}\n        \"\"\"\n        )\n\n\nclass IncompatibleEntry(InvalidBundle):\n    \"\"\"\n    Raised when the log entry within the `Bundle` has an incompatible KindVersion.\n    \"\"\"\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns diagnostics for the error.\"\"\"\n\n        return dedent(\n            f\"\"\"\\\n        The provided bundle contains a transparency log entry that is incompatible with this version of sigstore-python. Please upgrade your verifying client.\n\n        Additional context:\n\n        {self}\n        \"\"\"\n        )\n\n\nclass Bundle:\n    \"\"\"\n    Represents a Sigstore bundle.\n    \"\"\"\n\n    class BundleType(str, Enum):\n        \"\"\"\n        Known Sigstore bundle media types.\n        \"\"\"\n\n        BUNDLE_0_1 = \"application/vnd.dev.sigstore.bundle+json;version=0.1\"\n        BUNDLE_0_2 = \"application/vnd.dev.sigstore.bundle+json;version=0.2\"\n        BUNDLE_0_3_ALT = \"application/vnd.dev.sigstore.bundle+json;version=0.3\"\n        BUNDLE_0_3 = \"application/vnd.dev.sigstore.bundle.v0.3+json\"\n\n        def __str__(self) -> str:\n            \"\"\"Returns the variant's string value.\"\"\"\n            return self.value\n\n    def __init__(self, inner: _Bundle) -> None:\n        \"\"\"\n        Creates a new bundle. This is not a public API; use\n        `from_json` instead.\n\n        @private\n        \"\"\"\n        self._inner = inner\n        self._verify()\n\n    def _verify(self) -> None:\n        \"\"\"\n        Performs various feats of heroism to ensure the bundle is well-formed\n        and upholds invariants, including:\n\n        * The \"leaf\" (signing) certificate is present;\n        * There is a inclusion proof present, even if the Bundle's version\n           predates a mandatory inclusion proof.\n        \"\"\"\n\n        # The bundle must have a recognized media type.\n        try:\n            media_type = Bundle.BundleType(self._inner.media_type)\n        except ValueError:\n            raise InvalidBundle(f\"unsupported bundle format: {self._inner.media_type}\")\n\n        # Extract the signing certificate.\n        if media_type in (\n            Bundle.BundleType.BUNDLE_0_3,\n            Bundle.BundleType.BUNDLE_0_3_ALT,\n        ):\n            # For \"v3\" bundles, the signing certificate is the only one present.\n            if not self._inner.verification_material.certificate:\n                raise InvalidBundle(\"expected certificate in bundle\")\n\n            leaf_cert = load_der_x509_certificate(\n                self._inner.verification_material.certificate.raw_bytes\n            )\n        else:\n            # In older bundles, there is an entire pool (misleadingly called\n            # a chain) of certificates, the first of which is the signing\n            # certificate.\n            if not self._inner.verification_material.x509_certificate_chain:\n                raise InvalidBundle(\"expected certificate chain in bundle\")\n\n            chain = self._inner.verification_material.x509_certificate_chain\n            if not chain.certificates:\n                raise InvalidBundle(\"expected non-empty certificate chain in bundle\")\n\n            # Per client policy in protobuf-specs: the first entry in the chain\n            # MUST be a leaf certificate, and the rest of the chain MUST NOT\n            # include a root CA or any intermediate CAs that appear in an\n            # independent root of trust.\n            #\n            # We expect some old bundles to violate the rules around root\n            # and intermediate CAs, so we issue warnings and not hard errors\n            # in those cases.\n            leaf_cert, *chain_certs = (\n                load_der_x509_certificate(cert.raw_bytes) for cert in chain.certificates\n            )\n            if not cert_is_leaf(leaf_cert):\n                raise InvalidBundle(\n                    \"bundle contains an invalid leaf or non-leaf certificate in the leaf position\"\n                )\n\n            for chain_cert in chain_certs:\n                # TODO: We should also retrieve the root of trust here and\n                # cross-check against it.\n                if cert_is_root_ca(chain_cert):\n                    _logger.warning(\n                        \"this bundle contains a root CA, making it subject to misuse\"\n                    )\n\n        self._signing_certificate = leaf_cert\n\n        # Extract the log entry. For the time being, we expect\n        # bundles to only contain a single log entry.\n        tlog_entries = self._inner.verification_material.tlog_entries\n        if len(tlog_entries) != 1:\n            raise InvalidBundle(\"expected exactly one log entry in bundle\")\n        tlog_entry = tlog_entries[0]\n\n        if tlog_entry.kind_version.version not in [\"0.0.1\", \"0.0.2\"]:\n            raise IncompatibleEntry(\n                f\"Expected log entry version 0.0.1 - 0.0.2, got {tlog_entry.kind_version.version}\"\n            )\n\n        # Handling of inclusion promises and proofs varies between bundle\n        # format versions:\n        #\n        # * For 0.1, an inclusion promise is required; the client\n        #   MUST verify the inclusion promise.\n        #   The inclusion proof is NOT required. If provided, it might NOT\n        #   contain a checkpoint; in this case, we ignore it (since it's\n        #   useless without one).\n        #\n        # * For 0.2+, an inclusion proof is required; the client MUST\n        #   verify the inclusion proof. The inclusion prof MUST contain\n        #   a checkpoint.\n        #\n        #   The inclusion promise is NOT required if another source of signed\n        #   time (such as a signed timestamp) is present. If no other source\n        #   of signed time is present, then the inclusion promise MUST be\n        #   present.\n        #\n        # Before all of this, we require that the inclusion proof be present\n        # (when constructing the LogEntry).\n        log_entry = TransparencyLogEntry(tlog_entry)\n\n        if media_type == Bundle.BundleType.BUNDLE_0_1:\n            if not log_entry._inner.inclusion_promise:\n                raise InvalidBundle(\"bundle must contain an inclusion promise\")\n            if not log_entry._inner.inclusion_proof.checkpoint:\n                _logger.debug(\n                    \"0.1 bundle contains inclusion proof without checkpoint; ignoring\"\n                )\n        else:\n            if not log_entry._inner.inclusion_proof.checkpoint:\n                raise InvalidBundle(\"expected checkpoint in inclusion proof\")\n\n            if (\n                not log_entry._inner.inclusion_promise\n                and not self.verification_material.timestamp_verification_data\n            ):\n                raise InvalidBundle(\n                    \"bundle must contain an inclusion promise or signed timestamp(s)\"\n                )\n\n        self._log_entry = log_entry\n\n    @property\n    def signing_certificate(self) -> Certificate:\n        \"\"\"Returns the bundle's contained signing (i.e. leaf) certificate.\"\"\"\n        return self._signing_certificate\n\n    @property\n    def log_entry(self) -> TransparencyLogEntry:\n        \"\"\"\n        Returns the bundle's log entry, containing an inclusion proof\n        (with checkpoint) and an inclusion promise (if the latter is present).\n        \"\"\"\n        return self._log_entry\n\n    @property\n    def _dsse_envelope(self) -> dsse.Envelope | None:\n        \"\"\"\n        Returns the DSSE envelope within this Bundle as a `dsse.Envelope`.\n\n        @private\n        \"\"\"\n        if self._inner.dsse_envelope is not None:\n            return dsse.Envelope(self._inner.dsse_envelope)\n        return None\n\n    @property\n    def signature(self) -> bytes:\n        \"\"\"\n        Returns the signature bytes of this bundle.\n        Either from the DSSE Envelope or from the message itself.\n        \"\"\"\n        return (\n            self._dsse_envelope.signature\n            if self._dsse_envelope\n            else self._inner.message_signature.signature  # type: ignore[union-attr]\n        )\n\n    @property\n    def verification_material(self) -> VerificationMaterial:\n        \"\"\"\n        Returns the bundle's verification material.\n        \"\"\"\n        return VerificationMaterial(self._inner.verification_material)\n\n    @classmethod\n    def from_json(cls, raw: bytes | str) -> Bundle:\n        \"\"\"\n        Deserialize the given Sigstore bundle.\n        \"\"\"\n        try:\n            inner = _Bundle.from_json(raw)\n        except ValueError as exc:\n            raise InvalidBundle(f\"failed to load bundle: {exc}\")\n        return cls(inner)\n\n    def to_json(self) -> str:\n        \"\"\"\n        Return a JSON encoding of this bundle.\n        \"\"\"\n        return self._inner.to_json()\n\n    def _to_parts(\n        self,\n    ) -> tuple[Certificate, MessageSignature | dsse.Envelope, TransparencyLogEntry]:\n        \"\"\"\n        Decompose the `Bundle` into its core constituent parts.\n\n        @private\n        \"\"\"\n\n        content: MessageSignature | dsse.Envelope\n        if self._dsse_envelope:\n            content = self._dsse_envelope\n        else:\n            content = self._inner.message_signature  # type: ignore[assignment]\n\n        return (self.signing_certificate, content, self.log_entry)\n\n    @classmethod\n    def from_parts(\n        cls, cert: Certificate, sig: bytes, log_entry: TransparencyLogEntry\n    ) -> Bundle:\n        \"\"\"\n        Construct a Sigstore bundle (of `hashedrekord` type) from its\n        constituent parts.\n        \"\"\"\n\n        return cls._from_parts(\n            cert, MessageSignature(signature=base64.b64encode(sig)), log_entry\n        )\n\n    @classmethod\n    def _from_parts(\n        cls,\n        cert: Certificate,\n        content: MessageSignature | dsse.Envelope,\n        log_entry: TransparencyLogEntry,\n        signed_timestamp: list[TimeStampResponse] | None = None,\n    ) -> Bundle:\n        \"\"\"\n        @private\n        \"\"\"\n\n        timestamp_verifcation_data = bundle_v1.TimestampVerificationData(\n            rfc3161_timestamps=[]\n        )\n        if signed_timestamp is not None:\n            timestamp_verifcation_data.rfc3161_timestamps.extend(\n                [\n                    RFC3161SignedTimestamp(\n                        signed_timestamp=base64.b64encode(response.as_bytes())\n                    )\n                    for response in signed_timestamp\n                ]\n            )\n\n        # Fill in the appropriate variant.\n        message_signature = None\n        dsse_envelope = None\n        if isinstance(content, MessageSignature):\n            message_signature = content\n        else:\n            dsse_envelope = content._inner\n\n        inner = _Bundle(\n            media_type=Bundle.BundleType.BUNDLE_0_3.value,\n            verification_material=bundle_v1.VerificationMaterial(\n                certificate=common_v1.X509Certificate(\n                    raw_bytes=base64.b64encode(cert.public_bytes(Encoding.DER))\n                ),\n                tlog_entries=[log_entry._inner],\n                timestamp_verification_data=timestamp_verifcation_data,\n            ),\n            message_signature=message_signature,\n            dsse_envelope=dsse_envelope,\n        )\n\n        return cls(inner)\n\n\nclass SigningConfig:\n    \"\"\"\n    Signing configuration for a Sigstore instance.\n    \"\"\"\n\n    class SigningConfigType(str, Enum):\n        \"\"\"\n        Known Sigstore signing config media types.\n        \"\"\"\n\n        SIGNING_CONFIG_0_2 = \"application/vnd.dev.sigstore.signingconfig.v0.2+json\"\n\n        def __str__(self) -> str:\n            \"\"\"Returns the variant's string value.\"\"\"\n            return self.value\n\n    def __init__(\n        self, inner: trustroot_v1.SigningConfig, tlog_version: int | None = None\n    ):\n        \"\"\"\n        Construct a new `SigningConfig`.\n\n        tlog_version is an optional argument that enforces that only specified\n        versions of rekor are included in the transparency logs.\n\n        @api private\n        \"\"\"\n        self._inner = inner\n\n        # must have a recognized media type.\n        try:\n            SigningConfig.SigningConfigType(self._inner.media_type)\n        except ValueError:\n            raise Error(f\"unsupported signing config format: {self._inner.media_type}\")\n\n        # Create lists of service protos that are valid, selected by the service\n        # configuration & supported by this client\n        if tlog_version is None:\n            tlog_versions = REKOR_VERSIONS\n        else:\n            tlog_versions = [tlog_version]\n\n        self._tlogs = self._get_valid_services(\n            self._inner.rekor_tlog_urls, tlog_versions, self._inner.rekor_tlog_config\n        )\n        if not self._tlogs:\n            raise Error(\"No valid Rekor transparency log found in signing config\")\n\n        self._tsas = self._get_valid_services(\n            self._inner.tsa_urls, TSA_VERSIONS, self._inner.tsa_config\n        )\n\n        self._fulcios = self._get_valid_services(\n            self._inner.ca_urls, FULCIO_VERSIONS, None\n        )\n        if not self._fulcios:\n            raise Error(\"No valid Fulcio CA found in signing config\")\n\n        self._oidcs = self._get_valid_services(\n            self._inner.oidc_urls, OIDC_VERSIONS, None\n        )\n\n    @classmethod\n    def from_file(\n        cls,\n        path: str,\n    ) -> SigningConfig:\n        \"\"\"Create a new signing config from file\"\"\"\n        inner = trustroot_v1.SigningConfig.from_json(Path(path).read_bytes())\n        return cls(inner)\n\n    @staticmethod\n    def _get_valid_services(\n        services: list[trustroot_v1.Service],\n        supported_versions: list[int],\n        config: trustroot_v1.ServiceConfiguration | None,\n    ) -> list[trustroot_v1.Service]:\n        \"\"\"Return supported services, taking SigningConfig restrictions into account\"\"\"\n\n        # split services by operator, only include valid services\n        services_by_operator: dict[str, list[trustroot_v1.Service]] = defaultdict(list)\n        for service in services:\n            if service.major_api_version not in supported_versions:\n                continue\n\n            if not is_timerange_valid(service.valid_for, allow_expired=False):\n                continue\n\n            services_by_operator[service.operator].append(service)\n\n        # build a list of services but make sure we only include one service per operator\n        # and use the highest version available for that operator\n        result: list[trustroot_v1.Service] = []\n        for op_services in services_by_operator.values():\n            op_services.sort(key=lambda s: s.major_api_version)\n            result.append(op_services[-1])\n\n        # Depending on ServiceSelector, prune the result list\n        if not config or config.selector == trustroot_v1.ServiceSelector.ALL:\n            return result\n\n        # handle EXACT and ANY selectors\n        count = (\n            config.count\n            if config.selector == trustroot_v1.ServiceSelector.EXACT and config.count\n            else 1\n        )\n\n        if (\n            config.selector == trustroot_v1.ServiceSelector.EXACT\n            and len(result) < count\n        ):\n            raise ValueError(\n                f\"Expected {count} services in signing config, found {len(result)}\"\n            )\n\n        return result[:count]\n\n    def get_tlogs(self) -> list[RekorLogSubmitter]:\n        \"\"\"\n        Returns the rekor transparency log clients to sign with.\n        \"\"\"\n        result: list[RekorLogSubmitter] = []\n        for tlog in self._tlogs:\n            if tlog.major_api_version == 1:\n                from sigstore._internal.rekor.client import RekorClient\n\n                result.append(RekorClient(tlog.url))\n            elif tlog.major_api_version == 2:\n                from sigstore._internal.rekor.client_v2 import RekorV2Client\n\n                result.append(RekorV2Client(tlog.url))\n            else:\n                raise AssertionError(f\"Unexpected Rekor v{tlog.major_api_version}\")\n        return result\n\n    def get_fulcio(self) -> FulcioClient:\n        \"\"\"\n        Returns a Fulcio client to get a signing certificate from\n        \"\"\"\n        return FulcioClient(self._fulcios[0].url)\n\n    def get_oidc_url(self) -> str:\n        \"\"\"\n        Returns url for the OIDC provider that client should use to interactively\n        authenticate.\n        \"\"\"\n        if not self._oidcs:\n            raise Error(\"No valid OIDC provider found in signing config\")\n        return self._oidcs[0].url\n\n    def get_tsas(self) -> list[TimestampAuthorityClient]:\n        \"\"\"\n        Returns timestamp authority clients for urls configured in signing config.\n        \"\"\"\n        return [TimestampAuthorityClient(s.url) for s in self._tsas]\n\n\nclass TrustedRoot:\n    \"\"\"\n    The cryptographic root(s) of trust for a Sigstore instance.\n    \"\"\"\n\n    class TrustedRootType(str, Enum):\n        \"\"\"\n        Known Sigstore trusted root media types.\n        \"\"\"\n\n        TRUSTED_ROOT_0_1 = \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\"\n\n        def __str__(self) -> str:\n            \"\"\"Returns the variant's string value.\"\"\"\n            return self.value\n\n    def __init__(self, inner: trustroot_v1.TrustedRoot):\n        \"\"\"\n        Construct a new `TrustedRoot`.\n\n        @api private\n        \"\"\"\n        self._inner = inner\n        self._verify()\n\n    def _verify(self) -> None:\n        \"\"\"\n        Performs various feats of heroism to ensure that the trusted root\n        is well-formed.\n        \"\"\"\n\n        # The trusted root must have a recognized media type.\n        try:\n            TrustedRoot.TrustedRootType(self._inner.media_type)\n        except ValueError:\n            raise Error(f\"unsupported trusted root format: {self._inner.media_type}\")\n\n    @classmethod\n    def from_file(\n        cls,\n        path: str,\n    ) -> TrustedRoot:\n        \"\"\"Create a new trust root from file\"\"\"\n        inner = trustroot_v1.TrustedRoot.from_json(Path(path).read_bytes())\n        return cls(inner)\n\n    def _get_tlog_keys(\n        self, tlogs: list[trustroot_v1.TransparencyLogInstance], purpose: KeyringPurpose\n    ) -> Iterable[common_v1.PublicKey]:\n        \"\"\"\n        Yields an iterator of public keys for transparency log instances that\n        are suitable for `purpose`.\n        \"\"\"\n        allow_expired = purpose is KeyringPurpose.VERIFY\n        for tlog in tlogs:\n            if not is_timerange_valid(\n                tlog.public_key.valid_for, allow_expired=allow_expired\n            ):\n                continue\n\n            yield tlog.public_key\n\n    def rekor_keyring(self, purpose: KeyringPurpose) -> RekorKeyring:\n        \"\"\"Return keyring with keys for Rekor.\"\"\"\n\n        keys: list[common_v1.PublicKey] = list(\n            self._get_tlog_keys(self._inner.tlogs, purpose)\n        )\n        if len(keys) == 0:\n            raise MetadataError(\"Did not find any Rekor keys in trusted root\")\n        return RekorKeyring(Keyring(keys))\n\n    def ct_keyring(self, purpose: KeyringPurpose) -> CTKeyring:\n        \"\"\"Return keyring with key for CTFE.\"\"\"\n        ctfes: list[common_v1.PublicKey] = list(\n            self._get_tlog_keys(self._inner.ctlogs, purpose)\n        )\n        if not ctfes:\n            raise MetadataError(\"CTFE keys not found in trusted root\")\n        return CTKeyring(Keyring(ctfes))\n\n    def get_fulcio_certs(self) -> list[Certificate]:\n        \"\"\"Return the Fulcio certificates.\"\"\"\n\n        certs: list[Certificate] = []\n\n        # Return expired certificates too: they are expired now but may have\n        # been active when the certificate was used to sign.\n        for authority in self._inner.certificate_authorities:\n            certificate_authority = CertificateAuthority(authority)\n            certs.extend(certificate_authority.certificates(allow_expired=True))\n\n        if not certs:\n            raise MetadataError(\"Fulcio certificates not found in trusted root\")\n        return certs\n\n    def get_timestamp_authorities(self) -> list[CertificateAuthority]:\n        \"\"\"\n        Return the TSA present in the trusted root.\n\n        This list may be empty and in this case, no timestamp verification can be\n        performed.\n        \"\"\"\n        certificate_authorities: list[CertificateAuthority] = [\n            CertificateAuthority(cert_chain)\n            for cert_chain in self._inner.timestamp_authorities\n        ]\n        return certificate_authorities\n\n\nclass ClientTrustConfig:\n    \"\"\"\n    Represents a Sigstore client's trust configuration, including a root of trust.\n    \"\"\"\n\n    class ClientTrustConfigType(str, Enum):\n        \"\"\"\n        Known Sigstore client trust config media types.\n        \"\"\"\n\n        CONFIG_0_1 = \"application/vnd.dev.sigstore.clienttrustconfig.v0.1+json\"\n\n        def __str__(self) -> str:\n            \"\"\"Returns the variant's string value.\"\"\"\n            return self.value\n\n    @classmethod\n    def from_json(cls, raw: str) -> ClientTrustConfig:\n        \"\"\"\n        Deserialize the given client trust config.\n        \"\"\"\n        inner = trustroot_v1.ClientTrustConfig.from_json(raw)\n        return cls(inner)\n\n    @classmethod\n    def production(\n        cls,\n        offline: bool = False,\n    ) -> ClientTrustConfig:\n        \"\"\"Create new trust config from Sigstore production TUF repository.\n\n        If `offline`, will use data in local TUF cache. Otherwise will\n        update the data from remote TUF repository.\n        \"\"\"\n        return cls.from_tuf(DEFAULT_TUF_URL, offline)\n\n    @classmethod\n    def staging(\n        cls,\n        offline: bool = False,\n    ) -> ClientTrustConfig:\n        \"\"\"Create new trust config from Sigstore staging TUF repository.\n\n        If `offline`, will use data in local TUF cache. Otherwise will\n        update the data from remote TUF repository.\n        \"\"\"\n        return cls.from_tuf(STAGING_TUF_URL, offline)\n\n    @classmethod\n    def from_tuf(\n        cls,\n        url: str,\n        offline: bool = False,\n        bootstrap_root: Path | None = None,\n    ) -> ClientTrustConfig:\n        \"\"\"Create a new trust config from a TUF repository.\n\n        If `offline`, will use data in local TUF cache. Otherwise will\n        update the trust config from remote TUF repository.\n        \"\"\"\n        updater = TrustUpdater(url, offline, bootstrap_root)\n\n        tr_path = updater.get_trusted_root_path()\n        inner_tr = trustroot_v1.TrustedRoot.from_json(Path(tr_path).read_bytes())\n\n        try:\n            sc_path = updater.get_signing_config_path()\n            inner_sc = trustroot_v1.SigningConfig.from_json(Path(sc_path).read_bytes())\n        except TUFError as e:\n            raise e\n\n        return cls(\n            trustroot_v1.ClientTrustConfig(\n                media_type=ClientTrustConfig.ClientTrustConfigType.CONFIG_0_1.value,\n                trusted_root=inner_tr,\n                signing_config=inner_sc,\n            )\n        )\n\n    def __init__(self, inner: trustroot_v1.ClientTrustConfig) -> None:\n        \"\"\"\n        @api private\n        \"\"\"\n        self._inner = inner\n\n        # This can be used to enforce a specific rekor major version in signingconfig\n        self.force_tlog_version: int | None = None\n\n    @property\n    def trusted_root(self) -> TrustedRoot:\n        \"\"\"\n        Return the interior root of trust, as a `TrustedRoot`.\n        \"\"\"\n        return TrustedRoot(self._inner.trusted_root)\n\n    @property\n    def signing_config(self) -> SigningConfig:\n        \"\"\"\n        Return the interior root of trust, as a `SigningConfig`.\n        \"\"\"\n        return SigningConfig(\n            self._inner.signing_config, tlog_version=self.force_tlog_version\n        )\n"
  },
  {
    "path": "sigstore/oidc.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nAPI for retrieving OIDC tokens.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport sys\nimport time\nimport urllib.parse\nimport webbrowser\nfrom datetime import datetime, timezone\nfrom typing import NoReturn\n\nimport id\nimport jwt\nimport requests\nfrom pydantic import BaseModel, StrictStr\n\nfrom sigstore._internal import USER_AGENT\nfrom sigstore.errors import Error, NetworkError\n\n# See: https://github.com/sigstore/fulcio/blob/b2186c0/pkg/config/config.go#L182-L201\n_KNOWN_OIDC_ISSUERS = {\n    \"https://accounts.google.com\": \"email\",\n    \"https://oauth2.sigstore.dev/auth\": \"email\",\n    \"https://oauth2.sigstage.dev/auth\": \"email\",\n    \"https://token.actions.githubusercontent.com\": \"sub\",\n}\n\n_DEFAULT_CLIENT_ID = \"sigstore\"\n\n\nclass _OpenIDConfiguration(BaseModel):\n    \"\"\"\n    Represents a (subset) of the fields provided by an OpenID Connect provider's\n    `.well-known/openid-configuration` response, as defined by OpenID Connect Discovery.\n\n    See: <https://openid.net/specs/openid-connect-discovery-1_0.html>\n    \"\"\"\n\n    authorization_endpoint: StrictStr\n    token_endpoint: StrictStr\n\n\nclass ExpiredIdentity(Exception):\n    \"\"\"An error raised when an identity token is expired.\"\"\"\n\n\nclass IdentityToken:\n    \"\"\"\n    An OIDC \"identity\", corresponding to an underlying OIDC token with\n    a sensible subject, issuer, and audience for Sigstore purposes.\n    \"\"\"\n\n    def __init__(self, raw_token: str, client_id: str = _DEFAULT_CLIENT_ID) -> None:\n        \"\"\"\n        Create a new `IdentityToken` from the given OIDC token.\n        \"\"\"\n\n        self._raw_token = raw_token\n\n        # NOTE: The lack of verification here is intentional, and is part of\n        # Sigstore's verification model: clients like sigstore-python are\n        # responsible only for forwarding the OIDC identity to Fulcio for\n        # certificate binding and issuance.\n        try:\n            self._unverified_claims = jwt.decode(\n                raw_token,\n                options={\n                    \"verify_signature\": False,\n                    \"verify_aud\": True,\n                    \"verify_iat\": True,\n                    \"verify_exp\": True,\n                    # These claims are required by OpenID Connect, so\n                    # we can strongly enforce their presence.\n                    # See: https://openid.net/specs/openid-connect-basic-1_0.html#IDToken\n                    \"require\": [\"aud\", \"sub\", \"iat\", \"exp\", \"iss\"],\n                },\n                audience=client_id,\n                # NOTE: This leeway shouldn't be strictly necessary, but is\n                # included to preempt any (small) skew between the host\n                # and the originating IdP.\n                leeway=5,\n            )\n        except Exception as exc:\n            raise IdentityError(\n                \"Identity token is malformed or missing claims\"\n            ) from exc\n\n        self._iss: str = self._unverified_claims[\"iss\"]\n        self._nbf: int | None = self._unverified_claims.get(\"nbf\")\n        self._exp: int = self._unverified_claims[\"exp\"]\n\n        # Fail early if this token isn't within its validity period.\n        if not self.in_validity_period():\n            raise IdentityError(\"Identity token is not within its validity period\")\n\n        # When verifying the private key possession proof, Fulcio uses\n        # different claims depending on the token's issuer.\n        # We currently special-case a handful of these, and fall back\n        # on signing the \"sub\" claim otherwise.\n        identity_claim = _KNOWN_OIDC_ISSUERS.get(self.issuer)\n        if identity_claim is not None:\n            if identity_claim not in self._unverified_claims:\n                raise IdentityError(\n                    f\"Identity token is missing the required {identity_claim!r} claim\"\n                )\n\n            self._identity = str(self._unverified_claims.get(identity_claim))\n        else:\n            try:\n                self._identity = str(self._unverified_claims[\"sub\"])\n            except KeyError:\n                raise IdentityError(\n                    \"Identity token is missing the required 'sub' claim\"\n                )\n\n        # This identity token might have been retrieved directly from\n        # an identity provider, or it might be a \"federated\" identity token\n        # retrieved from a federated IdP (e.g., Sigstore's own Dex instance).\n        # In the latter case, the claims will also include a `federated_claims`\n        # set, which in turn should include a `connector_id` that reflects\n        # the \"real\" token issuer. We retrieve this, despite technically\n        # being an implementation detail, because it has value to client\n        # users: a client might want to make sure that its user is identifying\n        # with a *particular* IdP, which means that they need to pierce the\n        # federation layer to check which IdP is actually being used.\n        self._federated_issuer: str | None = None\n        federated_claims = self._unverified_claims.get(\"federated_claims\")\n        if federated_claims is not None:\n            if not isinstance(federated_claims, dict):\n                raise IdentityError(\n                    \"unexpected claim type: federated_claims is not a dict\"\n                )\n\n            federated_issuer = federated_claims.get(\"connector_id\")\n            if federated_issuer is not None:\n                if not isinstance(federated_issuer, str):\n                    raise IdentityError(\n                        \"unexpected claim type: federated_claims.connector_id is not a string\"\n                    )\n\n                self._federated_issuer = federated_issuer\n\n    def in_validity_period(self) -> bool:\n        \"\"\"\n        Returns whether or not this `Identity` is currently within its self-stated validity period.\n\n        NOTE: As noted in `Identity.__init__`, this is not a verifying wrapper;\n        the check here only asserts whether the *unverified* identity's claims\n        are within their validity period.\n        \"\"\"\n\n        now = datetime.now(timezone.utc).timestamp()\n\n        if self._nbf is not None:\n            return self._nbf <= now < self._exp\n        else:\n            return now < self._exp\n\n    @property\n    def identity(self) -> str:\n        \"\"\"\n        Returns this `IdentityToken`'s underlying \"subject\".\n\n        Note that this is **not** always the `sub` claim in the corresponding\n        identity token: depending onm the token's issuer, it may be a *different*\n        claim, such as `email`. This corresponds to the Sigstore ecosystem's\n        behavior, e.g. in each issued certificate's SAN.\n        \"\"\"\n        return self._identity\n\n    @property\n    def issuer(self) -> str:\n        \"\"\"\n        Returns a URL identifying this `IdentityToken`'s issuer.\n        \"\"\"\n        return self._iss\n\n    @property\n    def federated_issuer(self) -> str:\n        \"\"\"\n        Returns a URL identifying the **federated** issuer for any Sigstore\n        certificate issued against this identity token.\n\n        The behavior of this field is slightly subtle: for non-federated\n        identity providers (like a token issued directly by Google's IdP) it\n        should be exactly equivalent to `IdentityToken.issuer`. For federated\n        issuers (like Sigstore's own federated IdP) it should be equivalent to\n        the underlying federated issuer's URL, which is kept in an\n        implementation-defined claim.\n\n        This attribute exists so that clients who wish to inspect the expected\n        underlying issuer of their certificates can do so without relying on\n        implementation-specific behavior.\n        \"\"\"\n        if self._federated_issuer is not None:\n            return self._federated_issuer\n\n        return self.issuer\n\n    def __str__(self) -> str:\n        \"\"\"\n        Returns the underlying OIDC token for this identity.\n\n        That this token is secret in nature and **MUST NOT** be disclosed.\n        \"\"\"\n        return self._raw_token\n\n\nclass IssuerError(Exception):\n    \"\"\"\n    Raised on any communication or format error with an OIDC issuer.\n    \"\"\"\n\n    pass\n\n\nclass Issuer:\n    \"\"\"\n    Represents an OIDC issuer (IdP).\n    \"\"\"\n\n    def __init__(self, base_url: str) -> None:\n        \"\"\"\n        Create a new `Issuer` from the given base URL.\n\n        This URL is used to locate an OpenID Connect configuration file,\n        which is then used to bootstrap the issuer's state (such\n        as authorization and token endpoints).\n        \"\"\"\n        self.session = requests.Session()\n        self.session.headers.update({\"User-Agent\": USER_AGENT})\n\n        oidc_config_url = urllib.parse.urljoin(\n            f\"{base_url}/\", \".well-known/openid-configuration\"\n        )\n\n        try:\n            resp: requests.Response = self.session.get(oidc_config_url, timeout=30)\n        except (requests.ConnectionError, requests.Timeout) as exc:\n            raise NetworkError from exc\n\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            raise IssuerError from http_error\n\n        try:\n            # We don't generally expect this to fail (since the provider should\n            # return a non-success HTTP code which we catch above), but we\n            # check just in case we have a misbehaving OIDC issuer.\n            self.oidc_config = _OpenIDConfiguration.model_validate(resp.json())\n        except ValueError as exc:\n            raise IssuerError(f\"OIDC issuer returned invalid configuration: {exc}\")\n\n    def identity_token(  # nosec: B107\n        self,\n        client_id: str = _DEFAULT_CLIENT_ID,\n        client_secret: str = \"\",\n        force_oob: bool = False,\n    ) -> IdentityToken:\n        \"\"\"\n        Retrieves and returns an `IdentityToken` from the current `Issuer`, via OAuth.\n\n        This function blocks on user interaction.\n\n        The `force_oob` flag controls the kind of flow performed. When `False` (the default),\n        this function attempts to open the user's web browser before falling back to\n        an out-of-band flow. When `True`, the out-of-band flow is always used.\n        \"\"\"\n\n        # This function and the components that it relies on are based off of:\n        # https://github.com/psteniusubi/python-sample\n\n        from sigstore._internal.oidc.oauth import _OAuthFlow\n\n        code: str\n        with _OAuthFlow(client_id, client_secret, self) as server:\n            # Launch web browser\n            if not force_oob and webbrowser.open(server.base_uri):\n                print(\"Waiting for browser interaction...\", file=sys.stderr)\n            else:\n                server.enable_oob()\n                print(\n                    f\"Go to the following link in a browser:\\n\\n\\t{server.auth_endpoint}\",\n                    file=sys.stderr,\n                )\n\n            if not server.is_oob():\n                # Wait until the redirect server populates the response\n                while server.auth_response is None:\n                    time.sleep(0.1)\n\n                auth_error = server.auth_response.get(\"error\")\n                if auth_error is not None:\n                    raise IdentityError(\n                        f\"Error response from auth endpoint: {auth_error[0]}\"\n                    )\n\n                if server.auth_response[\"state\"][0] != server.oauth_session.state:\n                    raise IdentityError(\"OAuth state mismatch\")\n\n                code = server.auth_response[\"code\"][0]\n            else:\n                # In the out-of-band case, we wait until the user provides the code\n                code = input(\"Enter verification code: \")\n\n        # Provide code to token endpoint\n        data = {\n            \"grant_type\": \"authorization_code\",\n            \"redirect_uri\": server.redirect_uri,\n            \"code\": code,\n            \"code_verifier\": server.oauth_session.code_verifier,\n        }\n        auth = (\n            client_id,\n            client_secret,\n        )\n        logging.debug(f\"PAYLOAD: data={data}\")\n        try:\n            resp = self.session.post(\n                self.oidc_config.token_endpoint,\n                data=data,\n                auth=auth,\n                timeout=30,\n            )\n        except (requests.ConnectionError, requests.Timeout) as exc:\n            raise NetworkError from exc\n\n        try:\n            resp.raise_for_status()\n        except requests.HTTPError as http_error:\n            raise IdentityError(\n                f\"Token request failed with {resp.status_code}\"\n            ) from http_error\n\n        token_json = resp.json()\n        token_error = token_json.get(\"error\")\n        if token_error is not None:\n            raise IdentityError(f\"Error response from token endpoint: {token_error}\")\n\n        return IdentityToken(token_json[\"access_token\"], client_id)\n\n\nclass IdentityError(Error):\n    \"\"\"\n    Wraps `id`'s IdentityError.\n    \"\"\"\n\n    @classmethod\n    def raise_from_id(cls, exc: id.IdentityError) -> NoReturn:\n        \"\"\"Raises a wrapped IdentityError from the provided `id.IdentityError`.\"\"\"\n        raise cls(str(exc)) from exc\n\n    def diagnostics(self) -> str:\n        \"\"\"Returns diagnostics for the error.\"\"\"\n        if isinstance(self.__cause__, id.GitHubOidcPermissionCredentialError):\n            return f\"\"\"\n                Insufficient permissions for GitHub Actions workflow.\n\n                The most common reason for this is incorrect\n                configuration of the top-level `permissions` setting of the\n                workflow YAML file. It should be configured like so:\n\n                    permissions:\n                      id-token: write\n\n                Relevant documentation here:\n\n                    https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings\n\n                Another possible reason is that the workflow run has been\n                triggered by a PR from a forked repository. PRs from forked\n                repositories typically cannot be granted write access.\n\n                Relevant documentation here:\n\n                    https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token\n\n                Additional context:\n\n                {self.__cause__}\n                \"\"\"\n        else:\n            return f\"\"\"\n                An issue occurred with ambient credential detection.\n\n                Additional context:\n\n                {self}\n            \"\"\"\n\n\ndef detect_credential(client_id: str = _DEFAULT_CLIENT_ID) -> str | None:\n    \"\"\"Calls `id.detect_credential`, but wraps exceptions with our own exception type.\"\"\"\n\n    try:\n        return id.detect_credential(client_id)\n    except id.IdentityError as exc:\n        IdentityError.raise_from_id(exc)\n"
  },
  {
    "path": "sigstore/py.typed",
    "content": ""
  },
  {
    "path": "sigstore/sign.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nAPI for signing artifacts.\n\nExample:\n\n```python\nfrom pathlib import Path\nfrom sigstore.models import ClientTrustConfig\nfrom sigstore.oidc import Issuer\nfrom sigstore.sign import SigningContext\n\nartifact_path = Path(\"README.md\")\n\n# Construct OIDC Issuer and SigningContext for the\n# Sigstore Public Good instance\ntrust_config = ClientTrustConfig.production()\nissuer = Issuer(trust_config.signing_config.get_oidc_url())\ncontext = SigningContext.from_trust_config(trust_config)\n\n# Get an identity token from OIDC provider using interactive auth\ntoken = issuer.identity_token()\n\n# Sign artifact with the identity\nwith context.signer(token, cache = True) as signer:\n    bundle = signer.sign_artifact(artifact_path.read_bytes())\n\nwith Path(\"README.md.sigstore.json\").open(\"w\") as f:\n    f.write(bundle.to_json())\n```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport logging\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\nfrom datetime import datetime, timezone\n\nimport cryptography.x509 as x509\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.x509.oid import NameOID\nfrom sigstore_models.common.v1 import HashOutput, MessageSignature\n\nfrom sigstore import dsse\nfrom sigstore import hashes as sigstore_hashes\nfrom sigstore._internal.fulcio import (\n    ExpiredCertificate,\n    FulcioClient,\n)\nfrom sigstore._internal.rekor import EntryRequestBody, RekorLogSubmitter\nfrom sigstore._internal.sct import verify_sct\nfrom sigstore._internal.timestamp import TimestampAuthorityClient, TimestampError\nfrom sigstore._internal.trust import KeyringPurpose\nfrom sigstore._utils import sha256_digest\nfrom sigstore.models import Bundle, ClientTrustConfig, TrustedRoot\nfrom sigstore.oidc import ExpiredIdentity, IdentityToken\n\n_logger = logging.getLogger(__name__)\n\n\nclass Signer:\n    \"\"\"\n    The primary API for signing operations.\n    \"\"\"\n\n    def __init__(\n        self,\n        identity_token: IdentityToken,\n        signing_ctx: SigningContext,\n        cache: bool = True,\n    ) -> None:\n        \"\"\"\n        Create a new `Signer`.\n\n        `identity_token` is the identity token used to request a signing certificate\n        from Fulcio.\n\n        `signing_ctx` is a `SigningContext` that keeps information about the signing\n        configuration.\n\n        `cache` determines whether the signing certificate and ephemeral private key\n        should be reused (until the certificate expires) to sign different artifacts.\n        Default is `True`.\n        \"\"\"\n        self._identity_token = identity_token\n        self._signing_ctx: SigningContext = signing_ctx\n        self.__cached_private_key: ec.EllipticCurvePrivateKey | None = None\n        self.__cached_signing_certificate: x509.Certificate | None = None\n        if cache:\n            _logger.debug(\"Generating ephemeral keys...\")\n            self.__cached_private_key = ec.generate_private_key(ec.SECP256R1())\n            _logger.debug(\"Requesting ephemeral certificate...\")\n            self.__cached_signing_certificate = self._signing_cert()\n\n    @property\n    def _private_key(self) -> ec.EllipticCurvePrivateKey:\n        \"\"\"Get or generate a signing key.\"\"\"\n        if self.__cached_private_key is None:\n            _logger.debug(\"no cached key; generating ephemeral key\")\n            return ec.generate_private_key(ec.SECP256R1())\n        return self.__cached_private_key\n\n    def _signing_cert(\n        self,\n    ) -> x509.Certificate:\n        \"\"\"\n        Get or request a signing certificate from Fulcio.\n\n        Internally, this performs a CSR against Fulcio and verifies that\n        the returned certificate is present in Fulcio's CT log.\n        \"\"\"\n\n        # Our CSR cannot possibly succeed if our underlying identity token\n        # is expired.\n        if not self._identity_token.in_validity_period():\n            raise ExpiredIdentity\n\n        # If it exists, verify if the current certificate is expired\n        if self.__cached_signing_certificate:\n            not_valid_after = self.__cached_signing_certificate.not_valid_after_utc\n            if datetime.now(timezone.utc) > not_valid_after:\n                raise ExpiredCertificate\n            return self.__cached_signing_certificate\n\n        else:\n            _logger.debug(\"Retrieving signed certificate...\")\n\n            # Build an X.509 Certificate Signing Request\n            builder = (\n                x509.CertificateSigningRequestBuilder()\n                .subject_name(\n                    x509.Name(\n                        [\n                            x509.NameAttribute(\n                                NameOID.EMAIL_ADDRESS, self._identity_token._identity\n                            ),\n                        ]\n                    )\n                )\n                .add_extension(\n                    x509.BasicConstraints(ca=False, path_length=None),\n                    critical=True,\n                )\n            )\n            certificate_request = builder.sign(self._private_key, hashes.SHA256())\n\n            certificate_response = self._signing_ctx._fulcio.signing_cert.post(\n                certificate_request, self._identity_token\n            )\n\n            verify_sct(\n                certificate_response.cert,\n                certificate_response.chain,\n                self._signing_ctx._trusted_root.ct_keyring(KeyringPurpose.SIGN),\n            )\n\n            _logger.debug(\"Successfully verified SCT...\")\n\n            return certificate_response.cert\n\n    def _finalize_sign(\n        self,\n        cert: x509.Certificate,\n        content: MessageSignature | dsse.Envelope,\n        proposed_entry: EntryRequestBody,\n    ) -> Bundle:\n        \"\"\"\n        Perform the common \"finalizing\" steps in a Sigstore signing flow.\n        \"\"\"\n        # If the user provided TSA urls, timestamps the response\n        signed_timestamp = []\n        for tsa_client in self._signing_ctx._tsa_clients:\n            try:\n                signed_timestamp.append(tsa_client.request_timestamp(content.signature))\n            except TimestampError as e:\n                _logger.warning(\n                    f\"Unable to use {tsa_client.url} to timestamp the bundle. Failed with {e}\"\n                )\n\n        # Submit the proposed entry to the transparency log\n        entry = self._signing_ctx._rekor.create_entry(proposed_entry)\n        _logger.debug(\n            f\"Transparency log entry created with index: {entry._inner.log_index}\"\n        )\n\n        return Bundle._from_parts(cert, content, entry, signed_timestamp)\n\n    def sign_dsse(\n        self,\n        input_: dsse.Statement,\n    ) -> Bundle:\n        \"\"\"\n        Sign the given in-toto statement as a DSSE envelope, and return a\n        `Bundle` containing the signed result.\n\n        This API is **only** for in-toto statements; to sign arbitrary artifacts,\n        use `sign_artifact` instead.\n        \"\"\"\n        cert = self._signing_cert()\n\n        # Sign the statement, producing a DSSE envelope\n        content = dsse._sign(self._private_key, input_)\n\n        # Create the proposed DSSE log entry\n        proposed_entry = self._signing_ctx._rekor._build_dsse_request(\n            envelope=content, certificate=cert\n        )\n\n        return self._finalize_sign(cert, content, proposed_entry)\n\n    def sign_artifact(\n        self,\n        input_: bytes | sigstore_hashes.Hashed,\n    ) -> Bundle:\n        \"\"\"\n        Sign an artifact, and return a `Bundle` corresponding to the signed result.\n\n        The input can be one of two forms:\n\n        1. A `bytes` buffer;\n        2. A `Hashed` object, containing a pre-hashed input (e.g., for inputs\n           that are too large to buffer into memory).\n\n        Regardless of the input format, the signing operation will produce a\n        `hashedrekord` entry within the bundle. No other entry types\n        are supported by this API.\n        \"\"\"\n\n        cert = self._signing_cert()\n\n        # Sign artifact\n        hashed_input = sha256_digest(input_)\n\n        artifact_signature = self._private_key.sign(\n            hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed())\n        )\n\n        content = MessageSignature(\n            message_digest=HashOutput(\n                algorithm=hashed_input.algorithm,\n                digest=base64.b64encode(hashed_input.digest),\n            ),\n            signature=base64.b64encode(artifact_signature),\n        )\n\n        # Create the proposed hashedrekord entry\n        proposed_entry = self._signing_ctx._rekor._build_hashed_rekord_request(\n            hashed_input=hashed_input, signature=artifact_signature, certificate=cert\n        )\n\n        return self._finalize_sign(cert, content, proposed_entry)\n\n\nclass SigningContext:\n    \"\"\"\n    Keep a context between signing operations.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        fulcio: FulcioClient,\n        rekor: RekorLogSubmitter,\n        trusted_root: TrustedRoot,\n        tsa_clients: list[TimestampAuthorityClient] | None = None,\n    ):\n        \"\"\"\n        Create a new `SigningContext`.\n\n        `fulcio` is a `FulcioClient` capable of connecting to a Fulcio instance\n        and returning signing certificates.\n\n        `rekor` is a `RekorClient` capable of connecting to a Rekor instance\n        and creating transparency log entries.\n        \"\"\"\n        self._fulcio = fulcio\n        self._rekor = rekor\n        self._trusted_root = trusted_root\n        self._tsa_clients = tsa_clients or []\n\n    @classmethod\n    def from_trust_config(cls, trust_config: ClientTrustConfig) -> SigningContext:\n        \"\"\"\n        Create a `SigningContext` from the given `ClientTrustConfig`.\n\n        @api private\n        \"\"\"\n        signing_config = trust_config.signing_config\n        return cls(\n            fulcio=signing_config.get_fulcio(),\n            rekor=signing_config.get_tlogs()[0],\n            trusted_root=trust_config.trusted_root,\n            tsa_clients=signing_config.get_tsas(),\n        )\n\n    @contextmanager\n    def signer(\n        self, identity_token: IdentityToken, *, cache: bool = True\n    ) -> Iterator[Signer]:\n        \"\"\"\n        A context manager for signing operations.\n\n        `identity_token` is the identity token passed to the `Signer` instance\n        and used to request a signing certificate from Fulcio.\n\n        `cache` determines whether the signing certificate and ephemeral private key\n        generated by the `Signer` instance should be reused (until the certificate expires)\n        to sign different artifacts.\n        Default is `True`.\n        \"\"\"\n        yield Signer(identity_token, self, cache)\n"
  },
  {
    "path": "sigstore/verify/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nAPI for verifying artifact signatures.\n\nExample:\n```python\nimport base64\nfrom pathlib import Path\n\nfrom sigstore.models import Bundle\nfrom sigstore.verify import Verifier\nfrom sigstore.verify.policy import Identity\n\n# The input to verify\ninput_ = Path(\"foo.txt\").read_bytes()\n\n# The bundle to verify with\nbundle = Bundle.from_json(Path(\"foo.txt.sigstore.json\").read_bytes())\n\nverifier = Verifier.production()\nresult = verifier.verify(\n    input_,\n    bundle,\n    Identity(\n        identity=\"foo@bar.com\",\n        issuer=\"https://accounts.google.com\",\n    ),\n)\nprint(result)\n```\n\"\"\"\n\nfrom sigstore.verify import policy, verifier\nfrom sigstore.verify.verifier import Verifier\n\n__all__ = [\n    \"Verifier\",\n    \"policy\",\n    \"verifier\",\n]\n"
  },
  {
    "path": "sigstore/verify/policy.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nAPIs for describing identity verification \"policies\", which describe how the identities\npassed into an individual verification step are verified.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom abc import ABC, abstractmethod\nfrom typing import Protocol\n\nfrom cryptography.x509 import (\n    Certificate,\n    ExtensionNotFound,\n    ObjectIdentifier,\n    OtherName,\n    RFC822Name,\n    SubjectAlternativeName,\n    UniformResourceIdentifier,\n)\nfrom pyasn1.codec.der.decoder import decode as der_decode\nfrom pyasn1.type.char import UTF8String\n\nfrom sigstore.errors import VerificationError\n\n_logger = logging.getLogger(__name__)\n\n# From: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md\n_OIDC_ISSUER_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.1\")\n_OIDC_GITHUB_WORKFLOW_TRIGGER_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.2\")\n_OIDC_GITHUB_WORKFLOW_SHA_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.3\")\n_OIDC_GITHUB_WORKFLOW_NAME_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.4\")\n_OIDC_GITHUB_WORKFLOW_REPOSITORY_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.5\")\n_OIDC_GITHUB_WORKFLOW_REF_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.6\")\n_OTHERNAME_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.7\")\n_OIDC_ISSUER_V2_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.8\")\n_OIDC_BUILD_SIGNER_URI_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.9\")\n_OIDC_BUILD_SIGNER_DIGEST_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.10\")\n_OIDC_RUNNER_ENVIRONMENT_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.11\")\n_OIDC_SOURCE_REPOSITORY_URI_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.12\")\n_OIDC_SOURCE_REPOSITORY_DIGEST_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.13\")\n_OIDC_SOURCE_REPOSITORY_REF_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.14\")\n_OIDC_SOURCE_REPOSITORY_IDENTIFIER_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.15\")\n_OIDC_SOURCE_REPOSITORY_OWNER_URI_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.16\")\n_OIDC_SOURCE_REPOSITORY_OWNER_IDENTIFIER_OID = ObjectIdentifier(\n    \"1.3.6.1.4.1.57264.1.17\"\n)\n_OIDC_BUILD_CONFIG_URI_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.18\")\n_OIDC_BUILD_CONFIG_DIGEST_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.19\")\n_OIDC_BUILD_TRIGGER_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.20\")\n_OIDC_RUN_INVOCATION_URI_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.21\")\n_OIDC_SOURCE_REPOSITORY_VISIBILITY_OID = ObjectIdentifier(\"1.3.6.1.4.1.57264.1.22\")\n\n\nclass _SingleX509ExtPolicy(ABC):\n    \"\"\"\n    An ABC for verification policies that boil down to checking a single\n    X.509 extension's value.\n    \"\"\"\n\n    oid: ObjectIdentifier\n    \"\"\"\n    The OID of the extension being checked.\n    \"\"\"\n\n    def __init__(self, value: str) -> None:\n        \"\"\"\n        Creates the new policy, with `value` as the expected value during\n        verification.\n        \"\"\"\n        self._value = value\n\n    def verify(self, cert: Certificate) -> None:\n        \"\"\"\n        Verify this policy against `cert`.\n\n        Raises `VerificationError` on failure.\n        \"\"\"\n        try:\n            ext = cert.extensions.get_extension_for_oid(self.oid).value\n        except ExtensionNotFound:\n            raise VerificationError(\n                f\"Certificate does not contain {self.__class__.__name__} \"\n                f\"({self.oid.dotted_string}) extension\"\n            )\n\n        # NOTE(ww): mypy is confused by the `Extension[ExtensionType]` returned\n        # by `get_extension_for_oid` above.\n        ext_value = ext.value.decode()  # type: ignore[attr-defined]\n        if ext_value != self._value:\n            raise VerificationError(\n                f\"Certificate's {self.__class__.__name__} does not match \"\n                f\"(got '{ext_value}', expected '{self._value}')\"\n            )\n\n\nclass _SingleX509ExtPolicyV2(_SingleX509ExtPolicy):\n    \"\"\"\n    An base class for verification policies that boil down to checking a single\n    X.509 extension's value, where the value is formatted as a DER-encoded string,\n    the ASN.1 tag is UTF8String (0x0C) and the tag class is universal.\n    \"\"\"\n\n    def verify(self, cert: Certificate) -> None:\n        \"\"\"\n        Verify this policy against `cert`.\n\n        Raises `VerificationError` on failure.\n        \"\"\"\n        try:\n            ext = cert.extensions.get_extension_for_oid(self.oid).value\n        except ExtensionNotFound:\n            raise VerificationError(\n                f\"Certificate does not contain {self.__class__.__name__} \"\n                f\"({self.oid.dotted_string}) extension\"\n            )\n\n        # NOTE(ww): mypy is confused by the `Extension[ExtensionType]` returned\n        # by `get_extension_for_oid` above.\n        ext_value = der_decode(ext.value, UTF8String)[0].decode()  # type: ignore[attr-defined]\n        if ext_value != self._value:\n            raise VerificationError(\n                f\"Certificate's {self.__class__.__name__} does not match \"\n                f\"(got {ext_value}, expected {self._value})\"\n            )\n\n\nclass OIDCIssuer(_SingleX509ExtPolicy):\n    \"\"\"\n    Verifies the certificate's OIDC issuer, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.1`.\n    \"\"\"\n\n    oid = _OIDC_ISSUER_OID\n\n\nclass GitHubWorkflowTrigger(_SingleX509ExtPolicy):\n    \"\"\"\n    Verifies the certificate's GitHub Actions workflow trigger,\n    identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.2`.\n    \"\"\"\n\n    oid = _OIDC_GITHUB_WORKFLOW_TRIGGER_OID\n\n\nclass GitHubWorkflowSHA(_SingleX509ExtPolicy):\n    \"\"\"\n    Verifies the certificate's GitHub Actions workflow commit SHA,\n    identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.3`.\n    \"\"\"\n\n    oid = _OIDC_GITHUB_WORKFLOW_SHA_OID\n\n\nclass GitHubWorkflowName(_SingleX509ExtPolicy):\n    \"\"\"\n    Verifies the certificate's GitHub Actions workflow name,\n    identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.4`.\n    \"\"\"\n\n    oid = _OIDC_GITHUB_WORKFLOW_NAME_OID\n\n\nclass GitHubWorkflowRepository(_SingleX509ExtPolicy):\n    \"\"\"\n    Verifies the certificate's GitHub Actions workflow repository,\n    identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.5`.\n    \"\"\"\n\n    oid = _OIDC_GITHUB_WORKFLOW_REPOSITORY_OID\n\n\nclass GitHubWorkflowRef(_SingleX509ExtPolicy):\n    \"\"\"\n    Verifies the certificate's GitHub Actions workflow ref,\n    identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.6`.\n    \"\"\"\n\n    oid = _OIDC_GITHUB_WORKFLOW_REF_OID\n\n\nclass OIDCIssuerV2(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC issuer, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.8`.\n    The difference with `OIDCIssuer` is that the value for\n    this extension is formatted to the RFC 5280 specification\n    as a DER-encoded string.\n    \"\"\"\n\n    oid = _OIDC_ISSUER_V2_OID\n\n\nclass OIDCBuildSignerURI(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Build Signer URI, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.9`.\n    \"\"\"\n\n    oid = _OIDC_BUILD_SIGNER_URI_OID\n\n\nclass OIDCBuildSignerDigest(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Build Signer Digest, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.10`.\n    \"\"\"\n\n    oid = _OIDC_BUILD_SIGNER_DIGEST_OID\n\n\nclass OIDCRunnerEnvironment(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Runner Environment, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.11`.\n    \"\"\"\n\n    oid = _OIDC_RUNNER_ENVIRONMENT_OID\n\n\nclass OIDCSourceRepositoryURI(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Source Repository URI, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.12`.\n    \"\"\"\n\n    oid = _OIDC_SOURCE_REPOSITORY_URI_OID\n\n\nclass OIDCSourceRepositoryDigest(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Source Repository Digest, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.13`.\n    \"\"\"\n\n    oid = _OIDC_SOURCE_REPOSITORY_DIGEST_OID\n\n\nclass OIDCSourceRepositoryRef(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Source Repository Ref, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.14`.\n    \"\"\"\n\n    oid = _OIDC_SOURCE_REPOSITORY_REF_OID\n\n\nclass OIDCSourceRepositoryIdentifier(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Source Repository Identifier, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.15`.\n    \"\"\"\n\n    oid = _OIDC_SOURCE_REPOSITORY_IDENTIFIER_OID\n\n\nclass OIDCSourceRepositoryOwnerURI(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Source Repository Owner URI, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.16`.\n    \"\"\"\n\n    oid = _OIDC_SOURCE_REPOSITORY_OWNER_URI_OID\n\n\nclass OIDCSourceRepositoryOwnerIdentifier(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Source Repository Owner Identifier, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.17`.\n    \"\"\"\n\n    oid = _OIDC_SOURCE_REPOSITORY_OWNER_IDENTIFIER_OID\n\n\nclass OIDCBuildConfigURI(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Build Config URI, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.18`.\n    \"\"\"\n\n    oid = _OIDC_BUILD_CONFIG_URI_OID\n\n\nclass OIDCBuildConfigDigest(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Build Config Digest, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.19`.\n    \"\"\"\n\n    oid = _OIDC_BUILD_CONFIG_DIGEST_OID\n\n\nclass OIDCBuildTrigger(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Build Trigger, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.20`.\n    \"\"\"\n\n    oid = _OIDC_BUILD_TRIGGER_OID\n\n\nclass OIDCRunInvocationURI(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Run Invocation URI, identified by\n    an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.21`.\n    \"\"\"\n\n    oid = _OIDC_RUN_INVOCATION_URI_OID\n\n\nclass OIDCSourceRepositoryVisibility(_SingleX509ExtPolicyV2):\n    \"\"\"\n    Verifies the certificate's OIDC Source Repository Visibility\n    At Signing, identified by an X.509v3 extension tagged with\n    `1.3.6.1.4.1.57264.1.22`.\n    \"\"\"\n\n    oid = _OIDC_SOURCE_REPOSITORY_VISIBILITY_OID\n\n\nclass VerificationPolicy(Protocol):\n    \"\"\"\n    A protocol type describing the interface that all verification policies\n    conform to.\n    \"\"\"\n\n    @abstractmethod\n    def verify(self, cert: Certificate) -> None:\n        \"\"\"\n        Verify the given `cert` against this policy, raising `VerificationError`\n        on failure.\n        \"\"\"\n        raise NotImplementedError  # pragma: no cover\n\n\nclass AnyOf:\n    \"\"\"\n    The \"any of\" policy, corresponding to a logical OR between child policies.\n\n    An empty list of child policies is considered trivially invalid.\n    \"\"\"\n\n    def __init__(self, children: list[VerificationPolicy]):\n        \"\"\"\n        Create a new `AnyOf`, with the given child policies.\n        \"\"\"\n        self._children = children\n\n    def verify(self, cert: Certificate) -> None:\n        \"\"\"\n        Verify `cert` against the policy.\n\n        Raises `VerificationError` on failure.\n        \"\"\"\n\n        for child in self._children:\n            try:\n                child.verify(cert)\n            except VerificationError:\n                pass\n            else:\n                return\n\n        raise VerificationError(f\"0 of {len(self._children)} policies succeeded\")\n\n\nclass AllOf:\n    \"\"\"\n    The \"all of\" policy, corresponding to a logical AND between child\n    policies.\n\n    An empty list of child policies is considered trivially invalid.\n    \"\"\"\n\n    def __init__(self, children: list[VerificationPolicy]):\n        \"\"\"\n        Create a new `AllOf`, with the given child policies.\n        \"\"\"\n\n        self._children = children\n\n    def verify(self, cert: Certificate) -> None:\n        \"\"\"\n        Verify `cert` against the policy.\n        \"\"\"\n\n        # Without this, we'd consider empty lists of child policies trivially valid.\n        # This is almost certainly not what the user wants and is a potential\n        # source of API misuse, so we explicitly disallow it.\n        if len(self._children) < 1:\n            raise VerificationError(\"no child policies to verify\")\n\n        for child in self._children:\n            child.verify(cert)\n\n\nclass UnsafeNoOp:\n    \"\"\"\n    The \"no-op\" policy, corresponding to a no-op \"verification\".\n\n    **This policy is fundamentally insecure. You cannot use it safely.\n    It must not be used to verify any sort of certificate identity, because\n    it cannot do so. Using this policy is equivalent to reducing the\n    verification proof down to an integrity check against a completely\n    untrusted and potentially attacker-created signature. It must only\n    be used for testing purposes.**\n    \"\"\"\n\n    def verify(self, cert: Certificate) -> None:\n        \"\"\"\n        Verify `cert` against the policy.\n        \"\"\"\n\n        _logger.warning(\n            \"unsafe (no-op) verification policy used! no verification performed!\"\n        )\n\n\nclass Identity:\n    \"\"\"\n    Verifies the certificate's \"identity\", corresponding to the X.509v3 SAN.\n\n    Identities can be verified modulo an OIDC issuer, to prevent an unexpected\n    issuer from offering a particular identity.\n\n    Supported SAN types include emails, URIs, and Sigstore-specific \"other names\".\n    \"\"\"\n\n    _issuer: OIDCIssuer | None\n\n    def __init__(self, *, identity: str, issuer: str | None = None):\n        \"\"\"\n        Create a new `Identity`, with the given expected identity and issuer values.\n        \"\"\"\n\n        self._identity = identity\n        if issuer:\n            self._issuer = OIDCIssuer(issuer)\n        else:\n            self._issuer = None\n\n    def verify(self, cert: Certificate) -> None:\n        \"\"\"\n        Verify `cert` against the policy.\n        \"\"\"\n\n        if self._issuer:\n            self._issuer.verify(cert)\n\n        # Build a set of all valid identities.\n        san_ext = cert.extensions.get_extension_for_class(SubjectAlternativeName).value\n        all_sans = set(san_ext.get_values_for_type(RFC822Name))\n        all_sans.update(san_ext.get_values_for_type(UniformResourceIdentifier))\n        all_sans.update(\n            [\n                on.value.decode()\n                for on in san_ext.get_values_for_type(OtherName)\n                if on.type_id == _OTHERNAME_OID\n            ]\n        )\n\n        verified = self._identity in all_sans\n        if not verified:\n            raise VerificationError(\n                f\"Certificate's SANs do not match {self._identity}; actual SANs: {all_sans}\"\n            )\n"
  },
  {
    "path": "sigstore/verify/verifier.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nVerification API machinery.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport logging\nfrom datetime import datetime, timezone\nfrom typing import cast\n\nimport rekor_types\nfrom cryptography.exceptions import InvalidSignature\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage\nfrom cryptography.x509.oid import ExtendedKeyUsageOID\nfrom OpenSSL.crypto import (\n    X509,\n    X509Store,\n    X509StoreContext,\n    X509StoreContextError,\n    X509StoreFlags,\n)\nfrom pydantic import ValidationError\nfrom rfc3161_client import TimeStampResponse, VerifierBuilder\nfrom rfc3161_client import VerificationError as Rfc3161VerificationError\nfrom sigstore_models.common import v1\nfrom sigstore_models.rekor import v2\n\nfrom sigstore import dsse\nfrom sigstore._internal.rekor import _hashedrekord_from_parts\nfrom sigstore._internal.rekor.client import RekorClient\nfrom sigstore._internal.sct import (\n    verify_sct,\n)\nfrom sigstore._internal.timestamp import TimestampSource, TimestampVerificationResult\nfrom sigstore._internal.trust import KeyringPurpose\nfrom sigstore._utils import base64_encode_pem_cert, sha256_digest\nfrom sigstore.errors import CertValidationError, VerificationError\nfrom sigstore.hashes import Hashed\nfrom sigstore.models import Bundle, ClientTrustConfig, TrustedRoot\nfrom sigstore.verify.policy import VerificationPolicy\n\n_logger = logging.getLogger(__name__)\n\n# Limit the number of timestamps to prevent DoS\n# From https://github.com/sigstore/sigstore-go/blob/e92142f0734064ebf6001f188b7330a1212245fe/pkg/verify/tsa.go#L29\nMAX_ALLOWED_TIMESTAMP: int = 32\n\n# When verifying an entry, this threshold represents the minimum number of required\n# verified times to consider a signature valid.\nVERIFIED_TIME_THRESHOLD: int = 1\n\n\nclass Verifier:\n    \"\"\"\n    The primary API for verification operations.\n    \"\"\"\n\n    def __init__(self, *, trusted_root: TrustedRoot):\n        \"\"\"\n        Create a new `Verifier`.\n\n        `trusted_root` is the `TrustedRoot` object containing the root of trust\n        for the verification process.\n        \"\"\"\n        self._fulcio_certificate_chain: list[X509] = [\n            X509.from_cryptography(parent_cert)\n            for parent_cert in trusted_root.get_fulcio_certs()\n        ]\n        self._trusted_root = trusted_root\n\n        # this is an ugly hack needed for verifying \"detached\" materials\n        # In reality we should be choosing the rekor instance based on the logid\n        url = trusted_root._inner.tlogs[0].base_url\n        self._rekor = RekorClient(url)\n\n    @classmethod\n    def production(cls, *, offline: bool = False) -> Verifier:\n        \"\"\"\n        Return a `Verifier` instance configured against Sigstore's production-level services.\n\n        `offline` controls the Trusted Root refresh behavior: if `True`,\n        the verifier uses the Trusted Root in the local TUF cache. If `False`,\n        a TUF repository refresh is attempted.\n        \"\"\"\n        config = ClientTrustConfig.production(offline=offline)\n        return cls(\n            trusted_root=config.trusted_root,\n        )\n\n    @classmethod\n    def staging(cls, *, offline: bool = False) -> Verifier:\n        \"\"\"\n        Return a `Verifier` instance configured against Sigstore's staging-level services.\n\n        `offline` controls the Trusted Root refresh behavior: if `True`,\n        the verifier uses the Trusted Root in the local TUF cache. If `False`,\n        a TUF repository refresh is attempted.\n        \"\"\"\n        config = ClientTrustConfig.staging(offline=offline)\n        return cls(\n            trusted_root=config.trusted_root,\n        )\n\n    def _verify_signed_timestamp(\n        self, timestamp_response: TimeStampResponse, message: bytes\n    ) -> TimestampVerificationResult | None:\n        \"\"\"\n        Verify a Signed Timestamp using the TSA provided by the Trusted Root.\n        \"\"\"\n        cert_authorities = self._trusted_root.get_timestamp_authorities()\n        for certificate_authority in cert_authorities:\n            certificates = certificate_authority.certificates(allow_expired=True)\n\n            # We expect at least a signing cert and a root cert but there may be intermediates\n            if len(certificates) < 2:\n                _logger.debug(\"Unable to verify Timestamp: cert chain is incomplete\")\n                continue\n\n            builder = (\n                VerifierBuilder()\n                .tsa_certificate(certificates[0])\n                .add_root_certificate(certificates[-1])\n            )\n            for certificate in certificates[1:-1]:\n                builder = builder.add_intermediate_certificate(certificate)\n\n            verifier = builder.build()\n            try:\n                verifier.verify_message(timestamp_response, message)\n            except Rfc3161VerificationError:\n                _logger.debug(\"Unable to verify Timestamp with CA.\", exc_info=True)\n                continue\n\n            if (\n                certificate_authority.validity_period_start\n                <= timestamp_response.tst_info.gen_time\n            ) and (\n                not certificate_authority.validity_period_end\n                or timestamp_response.tst_info.gen_time\n                < certificate_authority.validity_period_end\n            ):\n                return TimestampVerificationResult(\n                    source=TimestampSource.TIMESTAMP_AUTHORITY,\n                    time=timestamp_response.tst_info.gen_time,\n                )\n\n            _logger.debug(\"Unable to verify Timestamp because not in CA time range.\")\n\n        return None\n\n    def _verify_timestamp_authority(\n        self, bundle: Bundle\n    ) -> list[TimestampVerificationResult]:\n        \"\"\"\n        Verify that the given bundle has been timestamped by a trusted timestamp authority\n        and that the timestamp is valid.\n\n        Returns the number of valid signed timestamp in the bundle.\n        \"\"\"\n        timestamp_responses = []\n        if (\n            timestamp_verification_data\n            := bundle.verification_material.timestamp_verification_data\n        ):\n            timestamp_responses = timestamp_verification_data.rfc3161_timestamps\n\n        if len(timestamp_responses) > MAX_ALLOWED_TIMESTAMP:\n            msg = f\"too many signed timestamp: {len(timestamp_responses)} > {MAX_ALLOWED_TIMESTAMP}\"\n            raise VerificationError(msg)\n\n        if len(set(timestamp_responses)) != len(timestamp_responses):\n            msg = \"duplicate timestamp found\"\n            raise VerificationError(msg)\n\n        verified_timestamps = [\n            result\n            for tsr in timestamp_responses\n            if (result := self._verify_signed_timestamp(tsr, bundle.signature))\n        ]\n\n        return verified_timestamps\n\n    def _establish_time(self, bundle: Bundle) -> list[TimestampVerificationResult]:\n        \"\"\"\n        Establish the time for bundle verification.\n\n        This method uses timestamps from two possible sources:\n        1. RFC3161 signed timestamps from a Timestamping Authority (TSA)\n        2. Transparency Log timestamps\n        \"\"\"\n        verified_timestamps = []\n\n        # If a timestamp from the timestamping service is available, the Verifier MUST\n        # perform path validation using the timestamp from the Timestamping Service.\n        if bundle.verification_material.timestamp_verification_data:\n            if not self._trusted_root.get_timestamp_authorities():\n                msg = (\n                    \"no Timestamp Authorities have been provided to validate this \"\n                    \"bundle but it contains a signed timestamp\"\n                )\n                raise VerificationError(msg)\n\n            timestamp_from_tsa = self._verify_timestamp_authority(bundle)\n            verified_timestamps.extend(timestamp_from_tsa)\n\n        # If a timestamp from the Transparency Service is available, the Verifier MUST\n        # perform path validation using the timestamp from the Transparency Service.\n        # NOTE: We only include this timestamp if it's accompanied by an inclusion\n        # promise that cryptographically binds it. We verify the inclusion promise\n        # itself later, as part of log entry verification.\n        if (\n            timestamp := bundle.log_entry._inner.integrated_time\n        ) and bundle.log_entry._inner.inclusion_promise:\n            kv = bundle.log_entry._inner.kind_version\n            if not (kv.kind in [\"dsse\", \"hashedrekord\"] and kv.version == \"0.0.1\"):\n                raise VerificationError(\n                    \"Integrated time only supported for dsse/hashedrekord 0.0.1 types\"\n                )\n\n            verified_timestamps.append(\n                TimestampVerificationResult(\n                    source=TimestampSource.TRANSPARENCY_SERVICE,\n                    time=datetime.fromtimestamp(timestamp, tz=timezone.utc),\n                )\n            )\n        return verified_timestamps\n\n    def _verify_chain_at_time(\n        self, certificate: X509, timestamp_result: TimestampVerificationResult\n    ) -> list[X509]:\n        \"\"\"\n        Verify the validity of the certificate chain at the given time.\n\n        Raises a VerificationError if the chain can't be built or be verified.\n        \"\"\"\n        # NOTE: The `X509Store` object cannot have its time reset once the `set_time`\n        # method been called on it. To get around this, we construct a new one in each\n        # call.\n        store = X509Store()\n        # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's\n        # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN\n        # would be strictly more conformant of OpenSSL, but we currently\n        # *want* the \"long\" chain behavior of performing path validation\n        # down to a self-signed root.\n        store.set_flags(X509StoreFlags.X509_STRICT)\n        for parent_cert_ossl in self._fulcio_certificate_chain:\n            store.add_cert(parent_cert_ossl)\n\n        store.set_time(timestamp_result.time)\n\n        store_ctx = X509StoreContext(store, certificate)\n\n        try:\n            # get_verified_chain returns the full chain including the end-entity certificate\n            # and chain should contain only CA certificates\n            return store_ctx.get_verified_chain()[1:]\n        except X509StoreContextError as e:\n            raise CertValidationError(\n                f\"failed to build timestamp certificate chain: {e}\"\n            )\n\n    def _verify_common_signing_cert(\n        self, bundle: Bundle, policy: VerificationPolicy\n    ) -> None:\n        \"\"\"\n        Performs the signing certificate verification steps that are shared between\n        `verify_dsse` and `verify_artifact`.\n\n        Raises `VerificationError` on all failures.\n        \"\"\"\n\n        # In order to verify an artifact, we need to achieve the following:\n        #\n        # 0. Establish a time for the signature.\n        # 1. Verify that the signing certificate chains to the root of trust\n        #    and is valid at the time of signing.\n        # 2. Verify the signing certificate's SCT.\n        # 3. Verify that the signing certificate conforms to the Sigstore\n        #    X.509 profile as well as the passed-in `VerificationPolicy`.\n        # 4. Verify the inclusion proof and signed checkpoint for the log\n        #    entry.\n        # 5. Verify the inclusion promise for the log entry, if present.\n        # 6. Verify the timely insertion of the log entry against the validity\n        #    period for the signing certificate.\n        # 7. Verify the signature and input against the signing certificate's\n        #    public key.\n        # 8. Verify the transparency log entry's consistency against the other\n        #    materials, to prevent variants of CVE-2022-36056.\n        #\n        # This method performs steps (0) through (6) above. Its caller\n        # MUST perform steps (7) and (8) separately, since they vary based on\n        # the kind of verification being performed (i.e. hashedrekord, DSSE, etc.)\n\n        cert = bundle.signing_certificate\n\n        # NOTE: The `X509Store` object currently cannot have its time reset once the `set_time`\n        # method been called on it. To get around this, we construct a new one for every `verify`\n        # call.\n        store = X509Store()\n        # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's\n        # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN\n        # would be strictly more conformant of OpenSSL, but we currently\n        # *want* the \"long\" chain behavior of performing path validation\n        # down to a self-signed root.\n        store.set_flags(X509StoreFlags.X509_STRICT)\n        for parent_cert_ossl in self._fulcio_certificate_chain:\n            store.add_cert(parent_cert_ossl)\n\n        # (0): Establishing a Time for the Signature\n        # First, establish verified times for the signature. This is required to\n        # validate the certificate chain, so this step comes first.\n        # These include TSA timestamps and (in the case of rekor v1 entries)\n        # rekor log integrated time.\n        verified_timestamps = self._establish_time(bundle)\n        if len(verified_timestamps) < VERIFIED_TIME_THRESHOLD:\n            raise VerificationError(\"not enough sources of verified time\")\n\n        # (1): verify that the signing certificate is signed by the root\n        #      certificate and that the signing certificate was valid at the\n        #      time of signing.\n        cert_ossl = X509.from_cryptography(cert)\n        chain: list[X509] = []\n        for vts in verified_timestamps:\n            chain = self._verify_chain_at_time(cert_ossl, vts)\n\n        # (2): verify the signing certificate's SCT.\n        try:\n            verify_sct(\n                cert,\n                [parent_cert.to_cryptography() for parent_cert in chain],\n                self._trusted_root.ct_keyring(KeyringPurpose.VERIFY),\n            )\n        except VerificationError as e:\n            raise VerificationError(f\"failed to verify SCT on signing certificate: {e}\")\n\n        # (3): verify the signing certificate against the Sigstore\n        #      X.509 profile and verify against the given `VerificationPolicy`.\n        usage_ext = cert.extensions.get_extension_for_class(KeyUsage)\n        if not usage_ext.value.digital_signature:\n            raise VerificationError(\"Key usage is not of type `digital signature`\")\n\n        extended_usage_ext = cert.extensions.get_extension_for_class(ExtendedKeyUsage)\n        if ExtendedKeyUsageOID.CODE_SIGNING not in extended_usage_ext.value:\n            raise VerificationError(\"Extended usage does not contain `code signing`\")\n\n        policy.verify(cert)\n\n        _logger.debug(\"Successfully verified signing certificate validity...\")\n\n        # (4): verify the inclusion proof and signed checkpoint for the\n        #      log entry.\n        # (5): verify the inclusion promise for the log entry, if present.\n        entry = bundle.log_entry\n        try:\n            entry._verify(self._trusted_root.rekor_keyring(KeyringPurpose.VERIFY))\n        except VerificationError as exc:\n            raise VerificationError(f\"invalid log entry: {exc}\")\n\n        # (6): verify our established times (timestamps or the log integration time) are\n        # within signing certificate validity period.\n        for vts in verified_timestamps:\n            if not (\n                bundle.signing_certificate.not_valid_before_utc\n                <= vts.time\n                <= bundle.signing_certificate.not_valid_after_utc\n            ):\n                raise VerificationError(\n                    f\"invalid signing cert: expired at time of signing, time via {vts}\"\n                )\n\n    def verify_dsse(\n        self, bundle: Bundle, policy: VerificationPolicy\n    ) -> tuple[str, bytes]:\n        \"\"\"\n        Verifies an bundle's DSSE envelope, returning the encapsulated payload\n        and its content type.\n\n        This method is only for DSSE-enveloped payloads. To verify\n        an arbitrary input against a bundle, use the `verify_artifact`\n        method.\n\n        `bundle` is the Sigstore `Bundle` to both verify and verify against.\n\n        `policy` is the `VerificationPolicy` to verify against.\n\n        Returns a tuple of `(type, payload)`, where `type` is the payload's\n        type as encoded in the DSSE envelope and `payload` is the raw `bytes`\n        of the payload. No validation of either `type` or `payload` is\n        performed; users of this API **must** assert that `type` is known\n        to them before proceeding to handle `payload` in an application-dependent\n        manner.\n        \"\"\"\n\n        # (1) through (6) are performed by `_verify_common_signing_cert`.\n        self._verify_common_signing_cert(bundle, policy)\n\n        # (7): verify the bundle's signature and DSSE envelope against the\n        #      signing certificate's public key.\n        envelope = bundle._dsse_envelope\n        if envelope is None:\n            raise VerificationError(\n                \"cannot perform DSSE verification on a bundle without a DSSE envelope\"\n            )\n\n        signing_key = bundle.signing_certificate.public_key()\n        signing_key = cast(ec.EllipticCurvePublicKey, signing_key)\n        dsse._verify(signing_key, envelope)\n\n        # (8): verify the consistency of the log entry's body against\n        #      the other bundle materials.\n        # NOTE: This is very slightly weaker than the consistency check\n        # for hashedrekord entries, due to how inclusion is recorded for DSSE:\n        # the included entry for DSSE includes an envelope hash that we\n        # *cannot* verify, since the envelope is uncanonicalized JSON.\n        # Instead, we manually pick apart the entry body below and verify\n        # the parts we can (namely the payload hash and signature list).\n        entry = bundle.log_entry\n        if entry._inner.kind_version.kind != \"dsse\":\n            raise VerificationError(\n                f\"Expected entry type dsse, got {entry._inner.kind_version.kind}\"\n            )\n        if entry._inner.kind_version.version == \"0.0.2\":\n            _validate_dsse_v002_entry_body(bundle)\n        elif entry._inner.kind_version.version == \"0.0.1\":\n            _validate_dsse_v001_entry_body(bundle)\n        else:\n            raise VerificationError(\n                f\"Unsupported dsse version {entry._inner.kind_version.version}\"\n            )\n\n        return (envelope._inner.payload_type, envelope._inner.payload)\n\n    def verify_artifact(\n        self,\n        input_: bytes | Hashed,\n        bundle: Bundle,\n        policy: VerificationPolicy,\n    ) -> None:\n        \"\"\"\n        Public API for verifying.\n\n        `input_` is the input to verify, either as a buffer of contents or as\n        a prehashed `Hashed` object.\n\n        `bundle` is the Sigstore `Bundle` to verify against.\n\n        `policy` is the `VerificationPolicy` to verify against.\n\n        On failure, this method raises `VerificationError`.\n        \"\"\"\n\n        # (1) through (6) are performed by `_verify_common_signing_cert`.\n        self._verify_common_signing_cert(bundle, policy)\n\n        hashed_input = sha256_digest(input_)\n        bundle_signature = bundle._inner.message_signature\n        if bundle_signature is None:\n            raise VerificationError(\"Missing bundle message signature\")\n\n        # signature is verified over input digest, but if the bundle documents the digest we still\n        # want to ensure it matches the input digest:\n        if (\n            bundle_signature.message_digest is not None\n            and hashed_input.digest != bundle_signature.message_digest.digest\n        ):\n            raise VerificationError(\"Bundle message digest mismatch\")\n\n        # (7): verify that the signature was signed by the public key in the signing certificate.\n        try:\n            signing_key = bundle.signing_certificate.public_key()\n            signing_key = cast(ec.EllipticCurvePublicKey, signing_key)\n            signing_key.verify(\n                bundle_signature.signature,\n                hashed_input.digest,\n                ec.ECDSA(hashed_input._as_prehashed()),\n            )\n        except InvalidSignature:\n            raise VerificationError(\"Signature is invalid for input\")\n\n        _logger.debug(\"Successfully verified signature...\")\n\n        # (8): verify the consistency of the log entry's body against\n        #      the other bundle materials (and input being verified).\n        entry = bundle.log_entry\n        if entry._inner.kind_version.kind != \"hashedrekord\":\n            raise VerificationError(\n                f\"Expected entry type hashedrekord, got {entry._inner.kind_version.kind}\"\n            )\n\n        if entry._inner.kind_version.version == \"0.0.2\":\n            _validate_hashedrekord_v002_entry_body(bundle, hashed_input)\n        elif entry._inner.kind_version.version == \"0.0.1\":\n            _validate_hashedrekord_v001_entry_body(bundle, hashed_input)\n        else:\n            raise VerificationError(\n                f\"Unsupported hashedrekord version {entry._inner.kind_version.version}\"\n            )\n\n\ndef _validate_dsse_v001_entry_body(bundle: Bundle) -> None:\n    \"\"\"\n    Validate the Entry body for dsse v001.\n    \"\"\"\n    entry = bundle.log_entry\n    envelope = bundle._dsse_envelope\n    if envelope is None:\n        raise VerificationError(\n            \"cannot perform DSSE verification on a bundle without a DSSE envelope\"\n        )\n    try:\n        entry_body = rekor_types.Dsse.model_validate_json(\n            entry._inner.canonicalized_body\n        )\n    except ValidationError as exc:\n        raise VerificationError(f\"invalid DSSE log entry: {exc}\")\n\n    payload_hash = sha256_digest(envelope._inner.payload).digest.hex()\n    if (\n        entry_body.spec.root.payload_hash.algorithm  # type: ignore[union-attr]\n        != rekor_types.dsse.Algorithm.SHA256\n    ):\n        raise VerificationError(\"expected SHA256 payload hash in DSSE log entry\")\n    if payload_hash != entry_body.spec.root.payload_hash.value:  # type: ignore[union-attr]\n        raise VerificationError(\"log entry payload hash does not match bundle\")\n\n    # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here,\n    # but we handle them just in case the signer has somehow produced multiple\n    # signatures for their envelope with the same signing key.\n    signatures = [\n        rekor_types.dsse.Signature(\n            signature=base64.b64encode(signature.sig).decode(),\n            verifier=base64_encode_pem_cert(bundle.signing_certificate),\n        )\n        for signature in envelope._inner.signatures\n    ]\n    if signatures != entry_body.spec.root.signatures:\n        raise VerificationError(\"log entry signatures do not match bundle\")\n\n\ndef _validate_dsse_v002_entry_body(bundle: Bundle) -> None:\n    \"\"\"\n    Validate Entry body for dsse v002.\n    \"\"\"\n    entry = bundle.log_entry\n    envelope = bundle._dsse_envelope\n    if envelope is None:\n        raise VerificationError(\n            \"cannot perform DSSE verification on a bundle without a DSSE envelope\"\n        )\n    try:\n        v2_body = v2.entry.Entry.from_json(entry._inner.canonicalized_body)\n    except ValidationError as exc:\n        raise VerificationError(f\"invalid DSSE log entry: {exc}\")\n\n    if v2_body.spec.dsse_v002 is None:\n        raise VerificationError(\"invalid DSSE log entry: missing dsse_v002 field\")\n\n    if v2_body.spec.dsse_v002.payload_hash.algorithm != v1.HashAlgorithm.SHA2_256:\n        raise VerificationError(\"expected SHA256 hash in DSSE entry\")\n\n    digest = sha256_digest(envelope._inner.payload).digest\n    if v2_body.spec.dsse_v002.payload_hash.digest != digest:\n        raise VerificationError(\"DSSE entry payload hash does not match bundle\")\n\n    v2_signatures = [\n        v2.verifier.Signature(\n            content=base64.b64encode(signature.sig),\n            verifier=_v2_verifier_from_certificate(bundle.signing_certificate),\n        )\n        for signature in envelope._inner.signatures\n    ]\n    if v2_signatures != v2_body.spec.dsse_v002.signatures:\n        raise VerificationError(\"log entry signatures do not match bundle\")\n\n\ndef _validate_hashedrekord_v001_entry_body(\n    bundle: Bundle, hashed_input: Hashed\n) -> None:\n    \"\"\"\n    Validate the Entry body for hashedrekord v001.\n    \"\"\"\n    entry = bundle.log_entry\n    expected_body = _hashedrekord_from_parts(\n        bundle.signing_certificate,\n        bundle._inner.message_signature.signature,  # type: ignore[union-attr]\n        hashed_input,\n    )\n    actual_body = rekor_types.Hashedrekord.model_validate_json(\n        entry._inner.canonicalized_body\n    )\n    if expected_body != actual_body:\n        raise VerificationError(\n            \"transparency log entry is inconsistent with other materials\"\n        )\n\n\ndef _validate_hashedrekord_v002_entry_body(\n    bundle: Bundle, hashed_input: Hashed\n) -> None:\n    \"\"\"\n    Validate Entry body for hashedrekord v002.\n    \"\"\"\n    entry = bundle.log_entry\n    if bundle._inner.message_signature is None:\n        raise VerificationError(\n            \"invalid hashedrekord log entry: missing message signature\"\n        )\n    v2_expected_body = v2.entry.Entry(\n        kind=entry._inner.kind_version.kind,\n        api_version=entry._inner.kind_version.version,\n        spec=v2.entry.Spec(\n            hashed_rekord_v002=v2.hashedrekord.HashedRekordLogEntryV002(\n                data=v1.HashOutput(\n                    algorithm=hashed_input.algorithm,\n                    digest=base64.b64encode(hashed_input.digest),\n                ),\n                signature=v2.verifier.Signature(\n                    content=base64.b64encode(bundle._inner.message_signature.signature),\n                    verifier=_v2_verifier_from_certificate(bundle.signing_certificate),\n                ),\n            )\n        ),\n    )\n    v2_actual_body = v2.entry.Entry.from_json(entry._inner.canonicalized_body)\n    if v2_expected_body != v2_actual_body:\n        raise VerificationError(\n            \"transparency log entry is inconsistent with other materials\"\n        )\n\n\ndef _v2_verifier_from_certificate(certificate: Certificate) -> v2.verifier.Verifier:\n    \"\"\"\n    Return a Rekor v2 Verifier for the signing certificate.\n\n    This method decides which signature algorithms are supported for verification\n    (in a rekor v2 entry), see\n    https://github.com/sigstore/architecture-docs/blob/main/algorithm-registry.md.\n    Note that actual signature verification happens in verify_artifact() and\n    verify_dsse(): New keytypes need to be added here and in those methods.\n    \"\"\"\n    public_key = certificate.public_key()\n\n    if isinstance(public_key, ec.EllipticCurvePublicKey):\n        if isinstance(public_key.curve, ec.SECP256R1):\n            key_details = v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256\n        elif isinstance(public_key.curve, ec.SECP384R1):\n            key_details = v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384\n        elif isinstance(public_key.curve, ec.SECP521R1):\n            key_details = v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512\n        else:\n            raise ValueError(f\"Unsupported EC curve: {public_key.curve.name}\")\n    else:\n        raise ValueError(f\"Unsupported public key type: {type(public_key)}\")\n\n    return v2.verifier.Verifier(\n        x509_certificate=v1.X509Certificate(\n            raw_bytes=base64.b64encode(\n                certificate.public_bytes(encoding=serialization.Encoding.DER)\n            )\n        ),\n        key_details=key_details,\n    )\n"
  },
  {
    "path": "test/assets/a.dsse.staging-rekor-v2.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"a.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json",
    "content": "{\"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\"}]}}\n"
  },
  {
    "path": "test/assets/a.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"a.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/a.txt.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEfjCCBAWgAwIBAgIUf/SsSCcPgO7o0yKoONG3Vz/RdzIwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjIwNzI4MTcwNDIxWhcNMjIwNzI4MTcxNDIxWjAAMHYwEAYH\nKoZIzj0CAQYFK4EEACIDYgAEiWxhv1x6hf4JJjbH8RSPxMs3DW4tLlQpbBOuVLxQ\nsUcxsA2mIW5N3O91Vikum0xT5NFsve8bH1vqOCuSdJItW0uesGJtJRB0aCQUnweC\nAWgTFcgObyYZdqfOOgYBp59Qo4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud\nJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBQdR6DA2mG5awJ5IeGQ/KwqhC8teTAf\nBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3\naWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo\ndWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb\nfBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYJFxDEtAAAEAQIAZtah\n4QQqmJIzhkoDb79Q1NOUMreaes/FxxkHAqPrVbLA0aSnA3NEwScd7P8PgwbRiFGz\nLILsACKHBlTmlzVB8s6qTtDrtjP8JHV7NSbxa84QSL0XcHUhMMVSGNvi5gr5dSom\nTJsrfxJTX1+uLRDvxhqTdGoPIl9/J9ekTMQ/C8WNlSwAJbQDsAFG4PZhu3M6haH3\ncH7l0VCDbewq42axDNlwbY7/jw9bvHrkU+PjSmRBOnyUuGJocHGDywqYA6vQLoO3\n/UcSWFQ+/QFHxGVC4f6SrM2c+GCPBTLPUVUAJEwi0U6OnB7d6ObLsMEy0vY56GPJ\nTGSJtUhxMFp/zDhMdBeviRDX1tLpT5rjP34F1Iee3KMZh7tEY45NlcIhdCJ8bAbt\nj+X7YVI15jrpX3XVBNilTgvAZy0/c4ZBjRcr44GwbHQvYE8N/9WEIf6d8I+9/d6S\nmRZJVNEcHEA+bpWGT/S7nzgxrtusFejR5JxxPIes8AVsFKNkCq0BkxiwRWIrIEEJ\n7TzdVZdZFc8OWthKlkVuylrbg2BJfARx7CMnywuj/gx23lf9B9IkgDRptPwJQCJm\n+AtNujk0BUTjh7VazWK+FBUqHDciZQEGDEksy24zMW+BxZwLUvQXpXUnKT9tzMLm\nYI7x0MeDKSd+lhi36Z5EJHyPGB0KPZ8KNRV49WIwCgYIKoZIzj0EAwMDZwAwZAIw\na/88UepYCuhkuUtwwL4WGsvEO4fN+07Togp9ksspYhpDvgEZSrn/oEwx7cNRbl7e\nAjBk/QR+Nif+U8CfM9bqs9SEww+KEykj+uyAud/C/qt97HsI7j7I2ECuy3SJL6vW\nl7I=\n-----END CERTIFICATE-----\n\n"
  },
  {
    "path": "test/assets/a.txt.sig",
    "content": "MGUCMQDVGmInKk9wYEfCmnp+kPnLYM/P5B9FXR8Ec7AoLRrq+qExIWS9gcg0GPPYbFkqX7gCMAsGbuVHKJedWNF6vnV4J+3p8u8MhKvBTP+gBVeSZU1CuvULwDfU15EDEwgitIBgiA==\n"
  },
  {
    "path": "test/assets/b.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"b.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/b.txt.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEfzCCBAWgAwIBAgIUXTEFeZc82/onXSK7L3gOx5ZryQYwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjIwNzI4MTcwNDIyWhcNMjIwNzI4MTcxNDIyWjAAMHYwEAYH\nKoZIzj0CAQYFK4EEACIDYgAE7fIULI0jGzRxDm/UhtOK2aYvsBkbc/J91AwXWRtu\nKEy1rPdf7MhP/IFyqQcvc9mBf2/tjBZEdgDDAZ7StmV9BHE3MgsYhkImsH4938mY\nEYHf6hzEsKkdiLjtHOcvAKX6o4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud\nJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBQ7bzu/2AekVQ/lsYw3MCxIw+WXzDAf\nBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3\naWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo\ndWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb\nfBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYJFxDShAAAEAQIAsI37\nokHyutfCBZKc0IAP8SgutX1wtA8cKfieySdpkWXnCqeiMjBpseEMaYIJPdpa3dwd\nAl1uLA5R24a9Oi6BDPJGFyZSrvRRtTz0oHKLrjVx0eDrL89KBb0Y554dkQH+XYS9\nPgZqOAUq1cubDJ1VTH8EEnhU7BSx4i8/Z1jZx1DFNIde+H+esWRmuOQmlk7ViT5W\nbir99BdvzfeBHtXqlSVygVfJgiJpJDwl85hBZ7ayKvuArN5bl6mISvfvt/W1fH+B\nsyn45LcjMqfRO+AwMP3Payv5/KbLV2WL9rfCkqCaR5Q94PHlh8KOAORVilay67kw\ng7+FvWGEHzYnk5iEV5/0gjtYwG1EnxsBMxNRtVVOhCLcQs3nfntYz794yvhd1S94\nzH0QVo3przPEU40hFjU+cOdNEnVJ0DkspoKVtM9fOrJbL9XXqVLCnUKQBRfwizNZ\n8GFEMi9VJ1lX1rybnP/SWWwiWsRTufWZ8ogpESUvSk3L/+XwFjuEy2gaWczODLAu\nPlj6f4YnHqAQxshf6Km7gUTodRw8yjXtYPPH1yxSZNJ1ivOUQdCS+I218SHloqsF\nDtHraHb68cRKSQXIsG8a1GGmlZ6ZOTpjFIq62qcRbrPPzTk1du5b0axEIaPHKPOs\nBwwKHrnZuVXE4yfwYXtciFgp8fsD8b8vBLJYH00wCgYIKoZIzj0EAwMDaAAwZQIx\nAPr8q3OZd7zY78xfVpbusI1Vg7tvwOUYFrAHWMc0n5QIrhwfUm7/ZTPKyzVB2An9\n2AIwS/qcggfuupePvsM7pHXXiDNexzTZBI9LP7evpXP32CM5cqebAn+kQFtxJ4jd\nafT2\n-----END CERTIFICATE-----\n\n"
  },
  {
    "path": "test/assets/b.txt.sig",
    "content": "MGYCMQCI3sfFtuJ+nyZEhK7HrJNE9OczNsXAJDOoE25rjLMb1sy8uSRdAEz9FORDSW9g6OsCMQCbroOxfpnr77LkvVZqbdRvnAaa3ZJBWXSnz1EiYnJ3OWBWp+699o9b8u0AxiPnofI=\n"
  },
  {
    "path": "test/assets/bad.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bad.txt\", a fiddled with input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bad.txt.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEfjCCBAWgAwIBAgIUf/SsSCcPgO7o0yKoONG3Vz/RdzIwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjIwNzI4MTcwNDIxWhcNMjIwNzI4MTcxNDIxWjAAMHYwEAYH\nKoZIzj0CAQYFK4EEACIDYgAEiWxhv1x6hf4JJjbH8RSPxMs3DW4tLlQpbBOuVLxQ\nsUcxsA2mIW5N3O91Vikum0xT5NFsve8bH1vqOCuSdJItW0uesGJtJRB0aCQUnweC\nAWgTFcgObyYZdqfOOgYBp59Qo4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud\nJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBQdR6DA2mG5awJ5IeGQ/KwqhC8teTAf\nBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3\naWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo\ndWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb\nfBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYJFxDEtAAAEAQIAZtah\n4QQqmJIzhkoDb79Q1NOUMreaes/FxxkHAqPrVbLA0aSnA3NEwScd7P8PgwbRiFGz\nLILsACKHBlTmlzVB8s6qTtDrtjP8JHV7NSbxa84QSL0XcHUhMMVSGNvi5gr5dSom\nTJsrfxJTX1+uLRDvxhqTdGoPIl9/J9ekTMQ/C8WNlSwAJbQDsAFG4PZhu3M6haH3\ncH7l0VCDbewq42axDNlwbY7/jw9bvHrkU+PjSmRBOnyUuGJocHGDywqYA6vQLoO3\n/UcSWFQ+/QFHxGVC4f6SrM2c+GCPBTLPUVUAJEwi0U6OnB7d6ObLsMEy0vY56GPJ\nTGSJtUhxMFp/zDhMdBeviRDX1tLpT5rjP34F1Iee3KMZh7tEY45NlcIhdCJ8bAbt\nj+X7YVI15jrpX3XVBNilTgvAZy0/c4ZBjRcr44GwbHQvYE8N/9WEIf6d8I+9/d6S\nmRZJVNEcHEA+bpWGT/S7nzgxrtusFejR5JxxPIes8AVsFKNkCq0BkxiwRWIrIEEJ\n7TzdVZdZFc8OWthKlkVuylrbg2BJfARx7CMnywuj/gx23lf9B9IkgDRptPwJQCJm\n+AtNujk0BUTjh7VazWK+FBUqHDciZQEGDEksy24zMW+BxZwLUvQXpXUnKT9tzMLm\nYI7x0MeDKSd+lhi36Z5EJHyPGB0KPZ8KNRV49WIwCgYIKoZIzj0EAwMDZwAwZAIw\na/88UepYCuhkuUtwwL4WGsvEO4fN+07Togp9ksspYhpDvgEZSrn/oEwx7cNRbl7e\nAjBk/QR+Nif+U8CfM9bqs9SEww+KEykj+uyAud/C/qt97HsI7j7I2ECuy3SJL6vW\nl7I=\n-----END CERTIFICATE-----\n\n"
  },
  {
    "path": "test/assets/bad.txt.sig",
    "content": "MGUCMQDVGmInKk9wYEfCmnp+kPnLYM/P5B9FXR8Ec7AoLRrq+qExIWS9gcg0GPPYbFkqX7gCMAsGbuVHKJedWNF6vnV4J+3p8u8MhKvBTP+gBVeSZU1CuvULwDfU15EDEwgitIBgiA==\n"
  },
  {
    "path": "test/assets/bundle.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bundle.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle.txt.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC5zCCAmygAwIBAgIUJ3vpewdf6e91rgjqCqagstF4qn8wCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjMwNDI2MDAyMTA4WhcNMjMwNDI2MDAzMTA4WjAAMHYwEAYH\nKoZIzj0CAQYFK4EEACIDYgAE2sd6+lOBcn5MXtnbwca7zcwpprl7GUZiKTO9IWpA\nUfVTtx+BXGHQCRwsFy/d7dLlf4hurIqhzMD5yaC2kcU9/8c9G55JyBXF8Dx5SQm9\ny2rPWFIdm29Ql9A3I3yyEFyPo4IBbjCCAWowDgYDVR0PAQH/BAQDAgeAMBMGA1Ud\nJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTlaUfjpiXGhBP3hOCW0JJZDSPxgzAf\nBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAYBgNVHREBAf8EDjAMgQph\nQHRueS50b3duMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dp\nbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dp\nbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5\nMZYC8pwzy15DQP6yrIZ6AAABh7rveBsAAAQDAEcwRQIhAKOZPMN9Q9qO1HXigHBP\nt+Ic16yy2Zgv2KQ23i5WLj16AiAzrFpuayGXdoK+hYePl9dEeXjG/vB2jK/E3sEs\nIrXtETAKBggqhkjOPQQDAwNpADBmAjEAgmhg80mI/Scr0isBnD5FYXZ8WxA8tnBB\nPmdf4aNGForGazGXaFQVPXgBVPv+YGI/AjEA0QzPC5dHD/WWXW2GbEC4dpwFk8OG\nRkiExMOy/+CqabbVg+/lx1N9VGBTlUTft45d\n-----END CERTIFICATE-----\n\n"
  },
  {
    "path": "test/assets/bundle.txt.sig",
    "content": "MGUCMQCOOJqTY6XWgB64izK2WVP07b0SG9M5WPCwKhfTPwMvtsgUi8KeRGwQkvvLYbKHdqUCMEbOXFG0NMqEQxWVb6rmGnexdADuGf6Jl8qAC8tn67p3QfVoXzMvFA61PzxwVwvb8g==\n"
  },
  {
    "path": "test/assets/bundle.txt.sigstore",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle+json;version=0.1\",\n    \"verificationMaterial\": {\n        \"x509CertificateChain\": {\n            \"certificates\": [\n                {\n                    \"rawBytes\": \"MIIC5zCCAmygAwIBAgIUJ3vpewdf6e91rgjqCqagstF4qn8wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwNDI2MDAyMTA4WhcNMjMwNDI2MDAzMTA4WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2sd6+lOBcn5MXtnbwca7zcwpprl7GUZiKTO9IWpAUfVTtx+BXGHQCRwsFy/d7dLlf4hurIqhzMD5yaC2kcU9/8c9G55JyBXF8Dx5SQm9y2rPWFIdm29Ql9A3I3yyEFyPo4IBbjCCAWowDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTlaUfjpiXGhBP3hOCW0JJZDSPxgzAfBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAYBgNVHREBAf8EDjAMgQphQHRueS50b3duMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABh7rveBsAAAQDAEcwRQIhAKOZPMN9Q9qO1HXigHBPt+Ic16yy2Zgv2KQ23i5WLj16AiAzrFpuayGXdoK+hYePl9dEeXjG/vB2jK/E3sEsIrXtETAKBggqhkjOPQQDAwNpADBmAjEAgmhg80mI/Scr0isBnD5FYXZ8WxA8tnBBPmdf4aNGForGazGXaFQVPXgBVPv+YGI/AjEA0QzPC5dHD/WWXW2GbEC4dpwFk8OGRkiExMOy/+CqabbVg+/lx1N9VGBTlUTft45d\"\n                }\n            ]\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"7390977\",\n                \"logId\": {\n                    \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1682468469\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEUCICSJs5PgN4W3Lku3ybrwfNLAKMWaOvffg2tnqm19VrWEAiEA16MVPsWDoaAljsxGefpQazpvYfs1pv8lzdgZQ0I4rH0=\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"7376158\",\n                    \"rootHash\": \"LE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=\",\n                    \"treeSize\": \"7376159\",\n                    \"hashes\": [\n                        \"zgesNHwk09VvW4IDaPrJMtX59glNyyLPzeJO1Gw1hCI=\",\n                        \"lJiFr9ZP5FO8BjqLAUQ16A/0/LoOOQ0gfeNhdxaxO2w=\",\n                        \"sMImd51DBHQnH1tz4sGk8gXB+FjWyusVXbP0GmpFnB4=\",\n                        \"cDU1nEpl0WCRlxLi/gNVzykDzobU4qG/7BQZxn0qDgU=\",\n                        \"4CRqWzG3qpxKvlHuZg5O6QjQiwOzerbjwsAh30EVlA8=\",\n                        \"Ru0p3GE/zB2zub2/xR5rY/aM4J+5VJmiIuIl2enF/ws=\",\n                        \"2W+NG5yGR68lrLGcw4gn9CSCfeQF98d3LMfdo8tPyok=\",\n                        \"bEs1eYxy9R6hR2veGEwYW4PEdrZKrdqZ7uDlmmNtlas=\",\n                        \"sgQMnwcK7VxxAi+fygxq8iJ+zWqShjXm07/AWobWcXU=\",\n                        \"y4BESazXFcefRzxpN1PfJHoqRaKnPJPM5H/jotx0QY8=\",\n                        \"xiNEdLOpmGQERCR+DCEFVRK+Ns6G0BLV9M6sQQkRhik=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstage.dev - 8050909264565447525\\n7376159\\nLE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=\\nTimestamp: 1682468469199678948\\n\\n\\u2014 rekor.sigstage.dev 0y8wozBEAiBbAodz3dBqJjGMhnZEkbaTDVxc8+tBEPKbaWUZoqxFvwIgGtYzFgFaM3UXBRHmzgmcrCxA145dpQ2YD0yFqiPHO7U=\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HVUNNUUNPT0pxVFk2WFdnQjY0aXpLMldWUDA3YjBTRzlNNVdQQ3dLaGZUUHdNdnRzZ1VpOEtlUkd3UWt2dkxZYktIZHFVQ01FYk9YRkcwTk1xRVF4V1ZiNnJtR25leGRBRHVHZjZKbDhxQUM4dG42N3AzUWZWb1h6TXZGQTYxUHp4d1Z3dmI4Zz09IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNMWVrTkRRVzE1WjBGM1NVSkJaMGxWU2pOMmNHVjNaR1kyWlRreGNtZHFjVU54WVdkemRFWTBjVzQ0ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwNUVTVEpOUkVGNVRWUkJORmRvWTA1TmFrMTNUa1JKTWsxRVFYcE5WRUUwVjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlRKelpEWXJiRTlDWTI0MVRWaDBibUozWTJFM2VtTjNjSEJ5YkRkSFZWcHBTMVJQT1VsWGNFRUtWV1pXVkhSNEswSllSMGhSUTFKM2MwWjVMMlEzWkV4c1pqUm9kWEpKY1doNlRVUTFlV0ZETW10alZUa3ZPR001UnpVMVNubENXRVk0UkhnMVUxRnRPUXA1TW5KUVYwWkpaRzB5T1ZGc09VRXpTVE41ZVVWR2VWQnZORWxDWW1wRFEwRlhiM2RFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pVYkdGVlptcHdhVmhIYUVKUU0yaFBRMWN3U2twYVJGTlFlR2Q2UVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpTZUdocVEyMUdTSGhwWWk5dU16RjJVVVpIYmpsbUx5dDBkbkpFUVZsQ1owNVdTRkpGUWtGbU9FVkVha0ZOWjFGd2FBcFJTRkoxWlZNMU1HSXpaSFZOUTNkSFEybHpSMEZSVVVKbk56aDNRVkZGUlVodGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1hOaU1tUndDbUpwT1haWldGWXdZVVJCZFVKbmIzSkNaMFZGUVZsUEwwMUJSVWxDUTBGTlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEFLWW1rNWRsbFlWakJoUkVOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRkRjM2QyVG5odmFVMXVhVFJrWjIxTFZqVXdTREJuTlFwTldsbERPSEIzZW5reE5VUlJVRFo1Y2tsYU5rRkJRVUpvTjNKMlpVSnpRVUZCVVVSQlJXTjNVbEZKYUVGTFQxcFFUVTQ1VVRseFR6RklXR2xuU0VKUUNuUXJTV014Tm5sNU1scG5kakpMVVRJemFUVlhUR294TmtGcFFYcHlSbkIxWVhsSFdHUnZTeXRvV1dWUWJEbGtSV1ZZYWtjdmRrSXlha3N2UlROelJYTUtTWEpZZEVWVVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1d1FVUkNiVUZxUlVGbmJXaG5PREJ0U1M5VFkzSXdhWE5DYmtRMVJsbFlXamhYZUVFNGRHNUNRZ3BRYldSbU5HRk9SMFp2Y2tkaGVrZFlZVVpSVmxCWVowSldVSFlyV1VkSkwwRnFSVUV3VVhwUVF6VmtTRVF2VjFkWVZ6SkhZa1ZETkdSd2QwWnJPRTlIQ2xKcmFVVjRUVTk1THl0RGNXRmlZbFpuS3k5c2VERk9PVlpIUWxSc1ZWUm1kRFExWkFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=\"\n        },\n        \"signature\": \"MGUCMQCOOJqTY6XWgB64izK2WVP07b0SG9M5WPCwKhfTPwMvtsgUi8KeRGwQkvvLYbKHdqUCMEbOXFG0NMqEQxWVb6rmGnexdADuGf6Jl8qAC8tn67p3QfVoXzMvFA61PzxwVwvb8g==\"\n    }\n}\n"
  },
  {
    "path": "test/assets/bundle_cve_2022_36056.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bundle_cve_2022_36056.txt\", a sample input for sigstore-python's unit tests.\n\nthis has a corresponding bundle that is valid, *except* that the included log entry\nis from a *valid but unrelated* bundle (specifically, for an identical input\nsigned immediately after this one).\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_cve_2022_36056.txt.sigstore",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle.v0.3+json\",\n    \"verificationMaterial\": {\n        \"certificate\": {\n            \"rawBytes\": \"MIIC1TCCAlugAwIBAgIUT8ug/4mjvLaDqXd4GKS6wmjq6MAwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDA1MjIwODEzWhcNMjQwNDA1MjIxODEzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECiYUx1SVwX5EHulBZv0FOEJ9AYXmCMOS8QVJnU1jY6xY6t4DCfaGwRU2iRIx8l4MmRKw8dwK8iA4/28TZt1HFKOCAXowggF2MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUvL83tyuyhCcA6zBgQlsrD9b2z5owHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjrBOHmkAAAQDAEgwRgIhAN/KC24XuwGgJRGpkvtzVVJSgEneKCV6PyM41Rul8gV0AiEA32ZU52ea/lCdPEzWTZxkdVbciAcsrATA+3D/o925g8owCgYIKoZIzj0EAwMDaAAwZQIxAO5FDiCQ79R69r6gyTgWhqADiisSZ7udiZGwRUWZcrBAYMKTw5Hy+1R/uKZcZ6jZKAIwFADtSVbmaXwC99hp++4aVyGo781VSiR5hIVRbFM+5l+psqG45/06bQy+Yj4EtrsY\"\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"26084047\",\n                \"logId\": {\n                    \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1712354907\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEUCIDMn04X1vVbjq+WBhC0jv9M3Py5KLujlCp9zaA5eUdNAAiEA3lalF4OHKNLmlKq06z2Zg9jtQYA7NxJ16zV6MglvHZI=\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"26069228\",\n                    \"rootHash\": \"Wr9rTCceIRRp9phvQmZTrPlNXo5b7i+9pIRkRSA9fG8=\",\n                    \"treeSize\": \"26069230\",\n                    \"hashes\": [\n                        \"flCB8VB67ZGa6K2ZEtDTtgtm96F3EjjtFvnGXwPOYT8=\",\n                        \"OzTdU4mq5jqXJ11gLmeEuCaLkxubkd4WVVwWUmZzgko=\",\n                        \"JV1urrvYBsls45EY/TJOuoRH3ho9y0nY1RvEgj1LWAs=\",\n                        \"VVpzU8MjvLgCT86Q0pSh57MzNiLGOphMU8kg9KAS9Lk=\",\n                        \"Nre+FErsP3TpqQY1RK7/b0WAL8fQx1bSbAuKjFSYvWg=\",\n                        \"jp/0CawpaDTbd+wM+aqjsO+AOVmIGunMId2ODziREU8=\",\n                        \"hSeZIoNlyUSqlJ6UyVfZIv17plm/YOvzrYEukkUh3OM=\",\n                        \"QdTMKazLZtCbvsCOn7U68L/vwKCJtgYyzRdxzbP3wcA=\",\n                        \"1P/q3R3vArPmJE+OmmcIRlBnXa/F2drYwklLngyaNXU=\",\n                        \"QyPS/J6veDqojEZv/v/8V1SpurFS22qOdFsQw1ZZH24=\",\n                        \"zL40ndFRmx2oQWFRdGwPjCl5BubNud42vN+OfvM9z9g=\",\n                        \"arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=\",\n                        \"Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=\",\n                        \"rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstage.dev - 8050909264565447525\\n26069230\\nWr9rTCceIRRp9phvQmZTrPlNXo5b7i+9pIRkRSA9fG8=\\n\\n\\u2014 rekor.sigstage.dev 0y8wozBFAiEAybn4EqPmEte82KeRUVEj5Kihrrm/72Bei84AF7CrPSwCIDANN3hLoyAiE5gN/3R2O4GRO+CvHZpsP2ZMB84X1Pa2\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjODdiNWIyMTNhYjA4YjMyOTcwYzEwNmQwYzdlNDQyM2U2N2Y1NDQ5YzJmZDJkMWU5YjRlYTZjYzQzYjZlZTdjIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURacnZDMjVxNGRlbStIb0liZ3BLNHI2MHZyUjhyay9CaEgvbVp3enROYXBnSWhBTm5KK1JFNXFiZ0xFR2lOeXN1OFVvS0lFL1diSWJaT1ZCL3hCVXZMbFRPZCIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhha05EUVd4MVowRjNTVUpCWjBsVlF6Rk9hbmRVZUU5eU1FZHhWMGNyZVZoUWNuWlZLMDVZUldWbmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVRGTmFrbDNUMFJKTTFkb1kwNU5hbEYzVGtSQk1VMXFTWGhQUkVrelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ0ZUVwQlVWQm5WWFpLTUZwWVJrdEVOVkV2WjJKSE0zRnZiMFFyYUdaVlRURTFWM1FLUm1aT1NUZHphemc0TVVNNFRUWXhSMDE2WWl0R2JsbEVhVkpUT0hCb1ZuQllWbEpyYUVOd1lWcEZZVloxSzJVeFVUWlBRMEZZYjNkblowWXlUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZyT0N0aUNtWXdMMFUzUkVwTGRVVlpNR3g0VjJwM1NUSlVZVUZyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwZDFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpsQ1NITkJaVkZDTTBGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFja0pQVmxCalFVRkJVVVJCUldkM1VtZEphRUZOYlZWSVVFOTRiWEE0VFdkblRtWndjbXRsUkdGbFdFbFZabnA1YlROVVEwdDVhV2xQUzNkd2VYQkdDa0ZwUlVFemRIWlVTbEJxV0dSMk1VMWtPSGd4WlRSWUt6RjFabWxWYUU5R1ZXdHFTVmxIVjA1NFpUQmFVeTlSZDBObldVbExiMXBKZW1vd1JVRjNUVVFLWVZGQmQxcG5TWGhCVFhNNVJGWmpNSHBEVjNSVmFFcEhPSEprVjJORGRHaHlNVmRZWkVGMllrODFSMFo2VEZsTVoyVlVhSFZJYzBZdk0yMXRkRmRpUWdwd1pVUkhZamtyZWpCblNYaEJVRmRRWVhFM1FqWllSamh5Y1ZwVFQzVTJVRWRwVFhOT2NXTTFRMmRPVUNzNFNWbFRVemt4V0U5aE1FRlJWamRoTUdzd0NtVkdVelZGZWtwNFRVMVVRMlJuUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"yHtbITqwizKXDBBtDH5EI+Z/VEnC/S0em06mzEO27nw=\"\n        },\n        \"signature\": \"MEUCIQCr7CB5uKBUYJQxVCiHA1kCZHusFBjKfI1G9cVcPfPDmAIgSzjMGvzMAI3/OvnDoVGWi2kVwXfuyCSqH/2EUjXA93o=\"\n    }\n}\n"
  },
  {
    "path": "test/assets/bundle_invalid_version.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bundle_invalid_versions.txt\", a sample input for sigstore-python's unit tests.\n\nthis has a corresponding bundle that is valid, *except* that the bundle's\nmedia type is nonsense.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_invalid_version.txt.sigstore",
    "content": "{\n    \"mediaType\": \"this is completely wrong\",\n    \"verificationMaterial\": {\n        \"certificate\": {\n            \"rawBytes\": \"MIIC1DCCAlmgAwIBAgIUfpvqrH5roQBYuCS0/ENuM/EIxNEwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwMzE1MjA0ODIyWhcNMjQwMzE1MjA1ODIyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExXe2NZ78guW75Lg7EalUisXCGK6+RhEMWiZtZnMliQ57FeS7VOhX+7yZ496R0e5K4y57q3xtA9rDpU2xG1hOC6OCAXgwggF0MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU3DU96/CFVHuUeFfcBFg54Gubt5IwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjkPfdqwAAAQDAEYwRAIgXu5ZYuDEPccUzU7hxM+c80mCgeSoBd3a3HXQTBY5RkACIHOcd+aHPP9q6WVBxZ1uz66LYUgttOv5vNLY4HoJHO8eMAoGCCqGSM49BAMDA2kAMGYCMQD1aJ1vd2ap0HY3ULvRnTpcZdYy5g5Hr4G3cGqSsjN+hVVqlUdvdmxNMnViF6riRBoCMQDuughnn3g3i6PL+rXhLzYRLnYndBbGpZtJNeOFBoKw5CJQ56v6Kep4QNFK/OHVS8w=\"\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"24949628\",\n                \"logId\": {\n                    \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1710535702\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEUCIQC0UGv+Vp65x5KxazYIsqUWtXRHt1YS3yLc6The2GB1vAIgY/OaE2hLlr8s1Q4Tcc3kQfaUFxn/ze5zSpOiONFtyDE=\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"24934809\",\n                    \"rootHash\": \"PDv3TU4otWhy8KPXPPenHw2Ewvhv39gLYJyb7y4T7Vg=\",\n                    \"treeSize\": \"24934810\",\n                    \"hashes\": [\n                        \"75BvLzwZrn9W1mMwiLlys6m8bdE6BzBtLoWb1TzD2IE=\",\n                        \"jeHvmA4WMzBKO4NTgU92d4SergID/98qHjQa7lyHM3I=\",\n                        \"PJ5j70iRcvuRdyqxfxTzriwR3DYCKxgn55n/x8bc88Q=\",\n                        \"fX6uhDvkI9uobrWeHxotNKFCnnQS6o+4DIx5VJEXPGI=\",\n                        \"Yi12x3CrLnY6ZtXzOD6Bko+pi9TgppbBd9Q/53BVAW4=\",\n                        \"CqWx5l0KONghRL4Y/KQCrseE3N5tobvzrSlwx6D72/Q=\",\n                        \"C4mAo6ST72aStQFQAjWnmHqGnNwkAbH/T1ZSGGW+oZQ=\",\n                        \"FMerZZ8JrR1y7HaRAWWMTHMF7Ogi+5ByNHzxnvd7wL4=\",\n                        \"Vq6uGE0RoCr4qvHzVA05MwXkyWEEB4quqDcBI4Xp7Yk=\",\n                        \"YEBzcKtzqBbRZdcxjYtCB3drVWOmpALLwzh9v3oDdGA=\",\n                        \"QTIhNTVhTR/6O+88G85QIfQsMUF4gIaq2ekPotTnXZc=\",\n                        \"hURLr2hArDJRWqYQcMBNoXVK/G0/rtoljhC5trcmdZ0=\",\n                        \"xj8ziVN4lEi2nf7WysmgKHG6LrsGd6QTwVBAL/yKdr0=\",\n                        \"SPEhngb75Zn+e/TNUQqKUKvY3Q7GE+M4BuniyqnCKLs=\",\n                        \"rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstage.dev - 8050909264565447525\\n24934810\\nPDv3TU4otWhy8KPXPPenHw2Ewvhv39gLYJyb7y4T7Vg=\\n\\n\\u2014 rekor.sigstage.dev 0y8wozBFAiAmS+VF6VZ1ylUPQ3v/bUIJwYKfNrdocQ1VTJz6lF6TOwIhAI7by9+dK+jqTsniPWlw1SL6gCGq4FLJ8Wz2evZZ8NPq\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3YTFmMmIyM2VjOWFkZDE2M2M2ZDBiYmI0NTRhZjQ1ZTE0ODNiOWJlYzdiNzJhZWExYjRmZGYwZTJmNTY0NGYwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQ2pZbnpKMXFiOE9IL3M4U3czUXg1VEJPMWM5bDFQK3NDTXBwcEJqN3VPd0FpQmY2cTE1R2VsN3JXdGUzMnR1VmtLVVpCZXJtVDZYTjRBL3NxL2tkWmhkdXc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4dFowRjNTVUpCWjBsVlpuQjJjWEpJTlhKdlVVSlpkVU5UTUM5RlRuVk5MMFZKZUU1RmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UlRGTmFrRXdUMFJKZVZkb1kwNU5hbEYzVFhwRk1VMXFRVEZQUkVsNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVY0V0dVeVRsbzNPR2QxVnpjMVRHYzNSV0ZzVldseldFTkhTellyVW1oRlRWZHBXblFLV201TmJHbFJOVGRHWlZNM1ZrOW9XQ3MzZVZvME9UWlNNR1UxU3pSNU5UZHhNM2gwUVRseVJIQlZNbmhITVdoUFF6WlBRMEZZWjNkblowWXdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlV6UkZVNUNqWXZRMFpXU0hWVlpVWm1ZMEpHWnpVMFIzVmlkRFZKZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwVVZsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpkQ1NHdEJaSGRDTVVGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFhMUJtWkhGM1FVRkJVVVJCUlZsM1VrRkpaMWgxTlZwWmRVUkZVR05qVlhwVk4yaDRUU3RqT0RCdFEyZGxVMjlDWkROaE0waFlVVlJDV1RWU2EwRkRDa2xJVDJOa0syRklVRkE1Y1RaWFZrSjRXakYxZWpZMlRGbFZaM1IwVDNZMWRrNU1XVFJJYjBwSVR6aGxUVUZ2UjBORGNVZFRUVFE1UWtGTlJFRXlhMEVLVFVkWlEwMVJSREZoU2pGMlpESmhjREJJV1ROVlRIWlNibFJ3WTFwa1dYazFaelZJY2pSSE0yTkhjVk56YWs0cmFGWldjV3hWWkhaa2JYaE9UVzVXYVFwR05uSnBVa0p2UTAxUlJIVjFaMmh1YmpObk0yazJVRXdyY2xob1RIcFpVa3h1V1c1a1FtSkhjRnAwU2s1bFQwWkNiMHQzTlVOS1VUVTJkalpMWlhBMENsRk9Sa3N2VDBoV1V6aDNQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"eh8rI+ya3RY8bQu7RUr0XhSDub7HtyrqG0/fDi9WRPA=\"\n        },\n        \"signature\": \"MEQCICjYnzJ1qb8OH/s8Sw3Qx5TBO1c9l1P+sCMpppBj7uOwAiBf6q15Gel7rWte32tuVkKUZBermT6XN4A/sq/kdZhduw==\"\n    }\n}\n"
  },
  {
    "path": "test/assets/bundle_no_cert_v1.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bundle_no_cert.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_no_cert_v1.txt.sigstore",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle+json;version=0.1\",\n    \"verificationMaterial\": {\n        \"x509CertificateChain\": {\n            \"certificates\": []\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"12299864\",\n                \"logId\": {\n                    \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1675126613\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEQCIHznNDQGR9OoggiqwdIy1XL+s0DIN8CKhy7HeeoL1TBLAiAbPK3/+x/j693cYidV0kKNf6qNQQcVQiYoDmc/GPSlNA==\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"8136433\",\n                    \"rootHash\": \"Q0UPuyoLnMKq1ovXXecjQ3T9S+Si3psOoufy+q8rXXo=\",\n                    \"treeSize\": \"8136434\",\n                    \"hashes\": [\n                        \"cdd3+Ki2g1oCwg8BSGNbjGjj1vnWCoW/bLvg6BTVewc=\",\n                        \"WUmDxZ3E04pjC/Boy8pxfDs0Buj3VTncmMNKpjJqsZ8=\",\n                        \"cVwcUBx0BZZQR36WQaQu0YM7QD7wv2rAAGdv9mbsl6A=\",\n                        \"upKFhQ0+3Te5YxqUVtD8w1JsYwvexrTLLRVvkiEzk4Y=\",\n                        \"M4k48iae5vhJ5K85ZwV5YJHrJXYwEQQxJgxeiIBR6O4=\",\n                        \"BaYLbIqmLbsAG8A+hzSvk3Blffx41WgBvn1c+HtvaPk=\",\n                        \"8SbpbSXXlm8lFn5KsRE6H+U+ZUj7cZd/JsBckNDHrY4=\",\n                        \"Xhw0UBkdQpGoX9d4nPr3dfz19Qxe1qKvPdbsEnuGpzQ=\",\n                        \"XrQ+ynp2Pi6q+yvC/JY+eAIoPPGpB2Y4JCF3sWaZQsA=\",\n                        \"VSPNAZ/qk9AYNPxCn3CLcArrcsg1pzFhzbkAP49OgHI=\",\n                        \"S232ZNlVK8JNSKTH1WWagnAGXh/tvDkQOwEKsjWoeFA=\",\n                        \"YWQp22x9IMWw7/Gm5RLqV6BzS5SuC8fJFGeUY+Aaf7o=\",\n                        \"WkrRU0sedw8Cv94N4VAIppcc8f+/qWP8nCpkSXOo0tE=\"\n                    ]\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIxYmMzN2Q0ZmVkM2ZkOTYwYmRmOWQwOTVmODcyODNiYThhZDFhYjQ0ODFmMzhhMzRlODAwNGYwYzU2ZmZlNzhkIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HVUNNR25uRUxCOXkzcStlUzdHSHJnYTJXZDBodWpXUTNMSjRXeVhoL1VMVWl3dXZmR0hOSzh6WFFuUUFOWmxSdEg2a1FJeEFKR0EvaXQ3T1ZiNDRBb3A2VDhETzVzK1RhamNuN0VnRng0MkRaaVdYU3JGZDBWTUl6bjRVRm80U0UxQy9VdkhhZz09IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VONFJFTkRRV3QxWjBGM1NVSkJaMGxWU1dwV1JVRlBkazFGY1ZVMU4zaDNPR3A1ZUhRMFkwZGhVa2hKZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwMVVUWGhOUkVFeFRtcFJNMWRvWTA1TmFrMTNUVlJOZUUxRVJYZE9hbEV6VjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlV4dVFrazNiRlEwTUhSU1owbE9ObGt3TlVNd01YZFJSMUZuY0dsTGFWZFhNVk13WkZGQ1RXWUtVME5IU2pOWFdEQm9hVlZQU0RSMVJHMVhZbXhCZGpscmNrUlVRa2RwTURoaGJHZDBSRzB5Y25rcmNqWjJTa3d6ZVZOc1RWUnNTMkZIVHpCNWNFWkZRd295VUhGdVpFZHhjMlkxYmtKdGNrVkVUWE5ZWldoWFZpOXZORWxDVkZSRFEwRlZhM2RFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pUUkhSQllqWlJVMGRJUjJkd2JtbHZjREJRZDNKVlYyVlBkVE5FUVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpVWmpBcmJsQldhVkZTYkhadGJ6SlBhMjlXWVV4SFRHaG9hMUI2UVhGQ1owNVdTRkpGUWtGbU9FVkpSRUZsWjFKNGFBcGlSMVkwVEcxT2FHSlhWbmxpTWpWQlpFaEthR0ZYZUhaYWJVcHdaRWhOZFZreU9YUk5RMnRIUTJselIwRlJVVUpuTnpoM1FWRkZSVWN5YURCa1NFSjZDazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRk9NRGtLVFVkeVIzaDRSWGxaZUd0bFNFcHNiazUzUzJsVGJEWTBNMnA1ZEM4MFpVdGpiMEYyUzJVMlQwRkJRVUpvWjFaVWFtVm5RVUZCVVVSQlJXTjNVbEZKYUFwQlRVOWhkVlVyVXpGeGJWQkJNblpOUzJ4QldHUlpiM1p0ZDNwUk5EVTVhMlV6TmxsdE1ERnZhRXRhVkVGcFFrRmhTa0pHVVdKSVNtRTVlREpGY0VsdkNraEZRVEUzTjBsWlpYVjRWVEJ5UkdGdU1sUkdWblJsWW1ORVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1dVFVUkNhMEZxUW0xc1ZqWjVOVFZOTWtaMWR6QUtPVUl6SzNkdmRFTjRhVWRCT1V4TGJUZHlWVzVFUVdaWFdYTlVWek5HTjJWaE1XVkplVkJsYlZvMVlXVnVTV3BoU0RGRlEwMUhSMUF2TjJoa1FYRnRhd3BNUkhoRFRFSmthbHBtTUROV2FVTnllSEpTVEVkQldVUjNRa2w2VERaemFWQlRiV3AyZDBSUFJWSkhOV05sVFVGVlRDOTJkbEU5UFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"G8N9T+0/2WC9+dCV+HKDuorRq0SB84o06ABPDFb/540=\"\n        },\n        \"signature\": \"MGUCMGnnELB9y3q+eS7GHrga2Wd0hujWQ3LJ4WyXh/ULUiwuvfGHNK8zXQnQANZlRtH6kQIxAJGA/it7OVb44Aop6T8DO5s+Tajcn7EgFx42DZiWXSrFd0VMIzn4UFo4SE1C/UvHag==\"\n    }\n}\n"
  },
  {
    "path": "test/assets/bundle_no_checkpoint.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bundle.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_no_checkpoint.txt.bundle",
    "content": ""
  },
  {
    "path": "test/assets/bundle_no_checkpoint.txt.crt",
    "content": "MGUCMArXoJGZeHwbgH1sCqhkv2f2J9XntOwIP1MrcXoqBsU3AAyeyB/1ggizV6ScbQFPtQIxAIoH4b4PCIbqufTc6UG4eTchZgYh5hW8m4BOkhbCEiCzKsaZ0Trg8+Hm1N8egtVgYw==\n"
  },
  {
    "path": "test/assets/bundle_no_checkpoint.txt.sigstore",
    "content": "{\"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=\"}}\n"
  },
  {
    "path": "test/assets/bundle_no_log_entry.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bundle_no_log_entry.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_no_log_entry.txt.sigstore",
    "content": "{\"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==\"}}\n"
  },
  {
    "path": "test/assets/bundle_v3.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is the input for bundle_v3, which tests support for \"v3\" bundles.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_v3.txt.sigstore",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle.v0.3+json\",\n    \"verificationMaterial\": {\n        \"certificate\": {\n            \"rawBytes\": \"MIIC1DCCAlqgAwIBAgIUO3tlVbLtvLPp+6zGOtep1SPkRigwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDAyMTkxOTA5WhcNMjQwNDAyMTkyOTA5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENdrfpgNU1Rjmz+j65rpJWKc08ruKYy4FX7nmmOnbauFZimsQXrdyDSXKNRtEXX4X3t/Amt+euwPDBh+eq7BCnqOCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUGRlBhD0wvzAfLb2dMWOgPrrJuRkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjqBAQZ4AAAQDAEcwRQIgeWUmtnD0MFUl5kkX7nbMdLWCsDGIPzdIlN+WaZF0TmkCIQC7+31saqrFe9RmduVZ2dxXhUPrajltuSDHb1vSGOcuHjAKBggqhkjOPQQDAwNoADBlAjEAn2+uuLHsnH9Db7zkIdF65YhiXbgMMF//iHc+B/QETK0HYVcOPTK3p46FUzXFD6xrAjAO2hrkfjBKANKjJJxHV3FVrtS+TR0GCP0HzC3D7Br95TXzfO7+j4Dd8/N/aAr6Ibs=\"\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"25915956\",\n                \"logId\": {\n                    \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1712085549\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEYCIQD2KXW1NppUhkPPzGR8NrUIyN+MzZSSqGZQO7CzvhSnYgIhAO9AHzjbsr1AHXRHmEpdPZcoFHEwwMTgfqwjoOXVMmqN\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"25901137\",\n                    \"rootHash\": \"iGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=\",\n                    \"treeSize\": \"25901138\",\n                    \"hashes\": [\n                        \"UHUr+lvxENI+G902oEsFW5ovQILgqO9mUWWxvvwHZZc=\",\n                        \"IcMBsbH3GRW8FX2CiL/ljMb45vzmENmhp5Yp/7IW998=\",\n                        \"SxC6nr0zP+a6kWb6nO2fmEtz8BYAbqEXc+dsqGLdRPM=\",\n                        \"sppZRSz/vdeLlavgvICrXHLeReMTJw98bs9HJ0I8WnE=\",\n                        \"c8lCSuBS6MzrRnt6OiyYjqhTyxUI/22gpVB7dblfDis=\",\n                        \"eJk64J6cMpIljPSX/72kH0kiIeElyypQm5vJ2gMMyHw=\",\n                        \"hbIK+jmAwQjU7Yi3iKvnfR1u7GNippk7QsRwJXIuRaw=\",\n                        \"tpHWIEB2vNU5ZmC68dj1Hh9cwQK083ozogA6zJ3cJ8A=\",\n                        \"arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=\",\n                        \"Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=\",\n                        \"rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstage.dev - 8050909264565447525\\n25901138\\niGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=\\n\\n\\u2014 rekor.sigstage.dev 0y8wozBFAiAMJJLbnNOnmizMbVBz9/A/qnMK15BudWoZkuE+obD6CAIhAJf6A3h2iOpuhz/duEhG3fbAQG9PXln4wXPHFBT5wT1a\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ZTZhZTlkZTU4YzExNzdiZWE2MTViNGZjYmZiMmZkNjg4ZThjNGI1MWMyZTU2YjZhMzhlODE3ODMzZWMyNGEyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRFFTSmk5YWVydFFobVQrY2UxaktOZENlNEtTY3NLR3E5ZlBtMzQyMkRCU0FpRUFoajFzeFo5Nm9ySVRzUXh5TUxJRFJKaW1wb3kxSjFNeWZsY1FWd2tremhzPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4eFowRjNTVUpCWjBsVlR6TjBiRlppVEhSMlRGQndLelo2UjA5MFpYQXhVMUJyVW1sbmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVhsTlZHdDRUMVJCTlZkb1kwNU5hbEYzVGtSQmVVMVVhM2xQVkVFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZPWkhKbWNHZE9WVEZTYW0xNksybzJOWEp3U2xkTFl6QTRjblZMV1hrMFJsZzNibTBLYlU5dVltRjFSbHBwYlhOUldISmtlVVJUV0V0T1VuUkZXRmcwV0ROMEwwRnRkQ3RsZFhkUVJFSm9LMlZ4TjBKRGJuRlBRMEZZYTNkblowWXhUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIVW14Q0NtaEVNSGQyZWtGbVRHSXlaRTFYVDJkUWNuSktkVkpyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwWjFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpoQ1NHOUJaVUZDTWtGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFjVUpCVVZvMFFVRkJVVVJCUldOM1VsRkpaMlZYVlcxMGJrUXdUVVpWYkRWcmExZzNibUpOWkV4WFEzTkVSMGxRZW1SSmJFNHJWMkZhUmpCVWJXdERDa2xSUXpjck16RnpZWEZ5Um1VNVVtMWtkVlphTW1SNFdHaFZVSEpoYW14MGRWTkVTR0l4ZGxOSFQyTjFTR3BCUzBKblozRm9hMnBQVUZGUlJFRjNUbThLUVVSQ2JFRnFSVUZ1TWl0MWRVeEljMjVJT1VSaU4zcHJTV1JHTmpWWmFHbFlZbWROVFVZdkwybElZeXRDTDFGRlZFc3dTRmxXWTA5UVZFc3pjRFEyUmdwVmVsaEdSRFo0Y2tGcVFVOHlhSEpyWm1wQ1MwRk9TMnBLU25oSVZqTkdWbkowVXl0VVVqQkhRMUF3U0hwRE0wUTNRbkk1TlZSWWVtWlBOeXRxTkVSa0NqZ3ZUaTloUVhJMlNXSnpQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"Xmrp3ljBF3vqYVtPy/sv1ojoxLUcLla2o46BeDPsJKI=\"\n        },\n        \"signature\": \"MEUCIDQSJi9aertQhmT+ce1jKNdCe4KScsKGq9fPm3422DBSAiEAhj1sxZ96orITsQxyMLIDRJimpoy1J1MyflcQVwkkzhs=\"\n    }\n}\n"
  },
  {
    "path": "test/assets/bundle_v3_alt.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is the input for bundle_v3_alt, which tests support for \"v3\" bundles\nwith the older (\"alternate\") v3 media type.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_v3_alt.txt.sigstore",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle+json;version=0.3\",\n    \"verificationMaterial\": {\n        \"certificate\": {\n            \"rawBytes\": \"MIIC1TCCAlqgAwIBAgIUM8X9bqAVFpaofemSrcgku8oJ/T8wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDAyMTkyMDE2WhcNMjQwNDAyMTkzMDE2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4tx/Vclr8Yr3ArUW9kIEFuR4mynLKKScX2ECX+I4WsXi6Q/0JUoVM2B/U3e97BZcl/bWHEToeshhWiQLaGztX6OCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU7611Rsd/9kcw/cE/Fe3B5fUfpt0wHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjqBBR8IAAAQDAEcwRQIhAOb1ZBDYuTNMa1RVLWvER4K51+ugnGYPCZKCXwx3hb+DAiBXX1DDn6Xv9B/RC5s3ZMQfbZ6jN7DFXQqDi/r3GLMP2DAKBggqhkjOPQQDAwNpADBmAjEAvXkFJvo2uuPZ7L5aRixEkysAF24+vRmISzcE2qTrGbzmqCW1AFzbnFsLhllo8IEJAjEAgEr9lfJZJCDLE1kV9M3/nfsPD/6ZNtDbU0vRjeygnXgsXJkrf96SjZCCfIlzxczF\"\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"25915997\",\n                \"logId\": {\n                    \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1712085616\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEUCIQCDbNwTMuX7lJt//HauYK0/RZ6UbKbYVR+vEr7rns4/ngIgSwRaRO2ody7uWMtIwe/ZRKwvl7+3Kn3IYKZDEj6CX8w=\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"25901178\",\n                    \"rootHash\": \"q0g3yMEVgKep9vgSfpTBZYld9mlsniTqXHzBAorxMtE=\",\n                    \"treeSize\": \"25901179\",\n                    \"hashes\": [\n                        \"6HxJ5B0YCXus8f+tO/yVTLFaLZfwjiaOnBOmhSzIo8k=\",\n                        \"Oa+3NjADjkBP1F7UrrJ8l7melp/y6mIlgHuEEGdSDrI=\",\n                        \"B4/zyNNgeuMr+zPZ/+mSVl//HFmVSxVWsNL1dHh4hw0=\",\n                        \"NzOg27Ucfb8sHqU9tZnKC5VZFuIsRpDYoqmBAPzB42g=\",\n                        \"SxC6nr0zP+a6kWb6nO2fmEtz8BYAbqEXc+dsqGLdRPM=\",\n                        \"sppZRSz/vdeLlavgvICrXHLeReMTJw98bs9HJ0I8WnE=\",\n                        \"c8lCSuBS6MzrRnt6OiyYjqhTyxUI/22gpVB7dblfDis=\",\n                        \"eJk64J6cMpIljPSX/72kH0kiIeElyypQm5vJ2gMMyHw=\",\n                        \"hbIK+jmAwQjU7Yi3iKvnfR1u7GNippk7QsRwJXIuRaw=\",\n                        \"tpHWIEB2vNU5ZmC68dj1Hh9cwQK083ozogA6zJ3cJ8A=\",\n                        \"arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=\",\n                        \"Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=\",\n                        \"rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstage.dev - 8050909264565447525\\n25901179\\nq0g3yMEVgKep9vgSfpTBZYld9mlsniTqXHzBAorxMtE=\\n\\n\\u2014 rekor.sigstage.dev 0y8wozBFAiAt/kYsQHQLeEo7R5UmNw7n7Mhn07ihpmFDC0zF1OfHSAIhAPCVUCdlUxnW7tz9Ob3IsX7e3St7pMwz32414GQZ6woa\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0MTkxZTZiNGUyYjAyNjBlNmUyOTdkNDc5N2QyYTg3MzU4NDk2NWFmZWYwMjFiZjIyZjJiYjdiZWM0MTEwMmQwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUNVeFo1V1Z0SG9oUEVIVzZwZ0hUQTBvMFoyWGdtUklGOEUvKzBQVGE5YWVRSWdPblRMZHNpYnhXbFpkVlNtckJzNEN3R2JuRXloT0dKc0Z0KzQ2anpjQU1VPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhWRU5EUVd4eFowRjNTVUpCWjBsVlRUaFlPV0p4UVZaR2NHRnZabVZ0VTNKaloydDFPRzlLTDFRNGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVhsTlZHdDVUVVJGTWxkb1kwNU5hbEYzVGtSQmVVMVVhM3BOUkVVeVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUwZEhndlZtTnNjamhaY2pOQmNsVlhPV3RKUlVaMVVqUnRlVzVNUzB0VFkxZ3lSVU1LV0N0Sk5GZHpXR2syVVM4d1NsVnZWazB5UWk5Vk0yVTVOMEphWTJ3dllsZElSVlJ2WlhOb2FGZHBVVXhoUjNwMFdEWlBRMEZZYTNkblowWXhUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlUzTmpFeENsSnpaQzg1YTJOM0wyTkZMMFpsTTBJMVpsVm1jSFF3ZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwWjFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpoQ1NHOUJaVUZDTWtGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFjVUpDVWpoSlFVRkJVVVJCUldOM1VsRkphRUZQWWpGYVFrUlpkVlJPVFdFeFVsWk1WM1pGVWpSTE5URXJkV2R1UjFsUVExcExRMWgzZUROb1lpdEVDa0ZwUWxoWU1VUkVialpZZGpsQ0wxSkROWE16V2sxUlptSmFObXBPTjBSR1dGRnhSR2t2Y2pOSFRFMVFNa1JCUzBKblozRm9hMnBQVUZGUlJFRjNUbkFLUVVSQ2JVRnFSVUYyV0d0R1NuWnZNblYxVUZvM1REVmhVbWw0Uld0NWMwRkdNalFyZGxKdFNWTjZZMFV5Y1ZSeVIySjZiWEZEVnpGQlJucGlia1p6VEFwb2JHeHZPRWxGU2tGcVJVRm5SWEk1YkdaS1drcERSRXhGTVd0V09VMHpMMjVtYzFCRUx6WmFUblJFWWxVd2RsSnFaWGxuYmxobmMxaEthM0ptT1RaVENtcGFRME5tU1d4NmVHTjZSZ290TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"QZHmtOKwJg5uKX1Hl9Koc1hJZa/vAhvyLyu3vsQRAtA=\"\n        },\n        \"signature\": \"MEUCIQCUxZ5WVtHohPEHW6pgHTA0o0Z2XgmRIF8E/+0PTa9aeQIgOnTLdsibxWlZdVSmrBs4CwGbnEyhOGJsFt+46jzcAMU=\"\n    }\n}\n"
  },
  {
    "path": "test/assets/bundle_v3_github.whl.sigstore",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle+json;version=0.2\",\n    \"verificationMaterial\": {\n        \"x509CertificateChain\": {\n            \"certificates\": [\n                {\n                    \"rawBytes\": \"MIIGzzCCBlSgAwIBAgIUM29bvYkrDKnBVZmVeloTUMlZqNYwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwMzE5MjI0MTE1WhcNMjQwMzE5MjI1MTE1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1q8wmpmK0vesCD05ZE1o5Jyu+g/CtLZLXNEZiIomh1jquPMCZrhlPdOfzQws+E+IUBX3pcVUxtn4rYKnMH39oaOCBXMwggVvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU0PaUbhtp84Orb2YatvZkIjkZiOEwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wZgYDVR0RAQH/BFwwWoZYaHR0cHM6Ly9naXRodWIuY29tL3RyYWlsb2ZiaXRzL3JmYzg3ODUucHkvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy90YWdzL3YwLjEuMjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBUGCisGAQQBg78wAQIEB3JlbGVhc2UwNgYKKwYBBAGDvzABAwQoZDhiNGE2NDQ1ZjM4YzQ4YjkxMzdhODA5OTcwNmQ5YjgwNzMxNDZlNDAVBgorBgEEAYO/MAEEBAdyZWxlYXNlMCQGCisGAQQBg78wAQUEFnRyYWlsb2ZiaXRzL3JmYzg3ODUucHkwHgYKKwYBBAGDvzABBgQQcmVmcy90YWdzL3YwLjEuMjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20waAYKKwYBBAGDvzABCQRaDFhodHRwczovL2dpdGh1Yi5jb20vdHJhaWxvZmJpdHMvcmZjODc4NS5weS8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbEByZWZzL3RhZ3MvdjAuMS4yMDgGCisGAQQBg78wAQoEKgwoZDhiNGE2NDQ1ZjM4YzQ4YjkxMzdhODA5OTcwNmQ5YjgwNzMxNDZlNDAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwOQYKKwYBBAGDvzABDAQrDClodHRwczovL2dpdGh1Yi5jb20vdHJhaWxvZmJpdHMvcmZjODc4NS5weTA4BgorBgEEAYO/MAENBCoMKGQ4YjRhNjQ0NWYzOGM0OGI5MTM3YTgwOTk3MDZkOWI4MDczMTQ2ZTQwIAYKKwYBBAGDvzABDgQSDBByZWZzL3RhZ3MvdjAuMS4yMBkGCisGAQQBg78wAQ8ECwwJNzY4MjEzOTk3MC4GCisGAQQBg78wARAEIAweaHR0cHM6Ly9naXRodWIuY29tL3RyYWlsb2ZiaXRzMBcGCisGAQQBg78wAREECQwHMjMxNDQyMzBoBgorBgEEAYO/MAESBFoMWGh0dHBzOi8vZ2l0aHViLmNvbS90cmFpbG9mYml0cy9yZmM4Nzg1LnB5Ly5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvdGFncy92MC4xLjIwOAYKKwYBBAGDvzABEwQqDChkOGI0YTY0NDVmMzhjNDhiOTEzN2E4MDk5NzA2ZDliODA3MzE0NmU0MBcGCisGAQQBg78wARQECQwHcmVsZWFzZTBcBgorBgEEAYO/MAEVBE4MTGh0dHBzOi8vZ2l0aHViLmNvbS90cmFpbG9mYml0cy9yZmM4Nzg1LnB5L2FjdGlvbnMvcnVucy84MzUxMDU4NTAxL2F0dGVtcHRzLzEwFgYKKwYBBAGDvzABFgQIDAZwdWJsaWMwgYoGCisGAQQB1nkCBAIEfAR6AHgAdgDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAY5Y4EK+AAAEAwBHMEUCIDagfjpw1AZX374vFXGDSZgJ9Kqrcq7Tk/Us3f7nmVQ1AiEA4esGBrDhflbIUujUmYC3eUWFFBgXHfABLiSDwciTQw8wCgYIKoZIzj0EAwMDaQAwZgIxAM6gKI5vKoqcvTkv87Foq3WXNYmAhPj3qaQ5ocXQXsWzHeNWGB6lSHTG3ENyapqYBgIxAMJW9ly3JXEdI5ydHfz+GZoh1kyc0XFUPp4V4kVjnUXY+KtoQWKSPHaZMkYC/szXhg==\"\n                }\n            ]\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"79605083\",\n                \"logId\": {\n                    \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1710888076\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEYCIQD8ohK48/Ls8D4Qd3dQZl6geplAt0p5Sgpa1wabniB/ZgIhALsVfKCe1m2KKtaEImxijm5bO2K49NltHWafJE2a1hnr\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"75441652\",\n                    \"rootHash\": \"uAqI3id6JHPMMNUltHIKHuX1kVHpm5y7jSfnbaRO+E4=\",\n                    \"treeSize\": \"75441653\",\n                    \"hashes\": [\n                        \"XoeIGlDW7f2lVjTlQEXPaV7szUXY2BECAEKtNA/lgfk=\",\n                        \"Pz5CyFQH78eikJoZuJ44Ls4R5najWJ1nKWunxb/vxeM=\",\n                        \"COo4wZnRb/d6zZOa7RP1euSRFb7H5EX5bYXs4HEQ0uU=\",\n                        \"1A4EnFDN5UCHjrJDWPuYDmY+ZLb4B+Jvis+k3ti+wjs=\",\n                        \"bBpWKtQryG7/tMDt9HDvKk/Fp3S+q7gTnYF56qGKMiI=\",\n                        \"ZR8qbYzXTNaK4SaofTZtbR0srNmOJ0Yx891OF5/G2gQ=\",\n                        \"7MueyMCRkh/GaluPkJl3xQFyXFq/SS9xykP299KtvS0=\",\n                        \"kFt/VRwfXksHcnd9vpdeifz3N16KyWQoDxAPfLlRwTA=\",\n                        \"gtt9e0foHZTCS9w+epNsmDWbwvX4FNV1EAg0rhxLfjg=\",\n                        \"BGqH+LzVuhuqCLiUvBJaB2hlsvtu2a15qq1WGw6mG44=\",\n                        \"OeS7D4kPES7ChE7kWSEmhbAMqBcKVj/z8/afMK4Y3pI=\",\n                        \"JtjqvAqFyXXYjWlZfDzElHpEzdBjsz1LmGFJuYx0kTU=\",\n                        \"s/ZIVcfcD4/nuZwUtQf4ydGsIAkGTPTzk3b0zhUC95k=\",\n                        \"YU1jZY/fp5tJdGF/i+/7ez8107O4/lOUp7acMPFEaOA=\",\n                        \"7Z18YLBAvejEV4nJHIKoks/xlijnhR005qTW2w4QtHg=\",\n                        \"98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstore.dev - 2605736670972794746\\n75441653\\nuAqI3id6JHPMMNUltHIKHuX1kVHpm5y7jSfnbaRO+E4=\\n\\n\\u2014 rekor.sigstore.dev wNI9ajBGAiEA5perJLLm94gCQOQT5/vO29OXWNZ1SoengZDZ/U6vsOUCIQDBL0BIkCjWGR6V622znnVpXF5D1g0jPgajBlHh8uSc8g==\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjNGU5MmU5ZWNjODI4YmVmMmFhN2RiYTFkZThhYzk4MzUxMWY3NTMyYTBkZjExYzc3MGQzOTA5OWEyNWNmMjAxIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNlSDZFM01wWm5nV0E2UlBnOEhBbC9aNzY0aFRGWXljTnlGM1IrbVBUU2JBSWhBUGdNUzhxQk04bENFVTJYVzc2NW15TU16Mnp1eXU5aVRGNDBQSCtYWmxKUSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVZDZla05EUW14VFowRjNTVUpCWjBsVlRUSTVZblpaYTNKRVMyNUNWbHB0Vm1Wc2IxUlZUV3hhY1U1WmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UlRWTmFra3dUVlJGTVZkb1kwNU5hbEYzVFhwRk5VMXFTVEZOVkVVeFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVV4Y1RoM2JYQnRTekIyWlhORFJEQTFXa1V4YnpWS2VYVXJaeTlEZEV4YVRGaE9SVm9LYVVsdmJXZ3hhbkYxVUUxRFduSm9iRkJrVDJaNlVYZHpLMFVyU1ZWQ1dETndZMVpWZUhSdU5ISlpTMjVOU0RNNWIyRlBRMEpZVFhkbloxWjJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlV3VUdGVkNtSm9kSEE0TkU5eVlqSlpZWFIyV210SmFtdGFhVTlGZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDFwbldVUldVakJTUVZGSUwwSkdkM2RYYjFwWllVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcFlWaFNlZ3BNTTBwdFdYcG5NMDlFVlhWalNHdDJURzFrY0dSSGFERlphVGt6WWpOS2NscHRlSFprTTAxMlkyMVdjMXBYUm5wYVV6VTFZbGQ0UVdOdFZtMWplVGt3Q2xsWFpIcE1NMWwzVEdwRmRVMXFRVFZDWjI5eVFtZEZSVUZaVHk5TlFVVkNRa04wYjJSSVVuZGplbTkyVEROU2RtRXlWblZNYlVacVpFZHNkbUp1VFhVS1dqSnNNR0ZJVm1sa1dFNXNZMjFPZG1KdVVteGlibEYxV1RJNWRFMUNWVWREYVhOSFFWRlJRbWMzT0hkQlVVbEZRak5LYkdKSFZtaGpNbFYzVG1kWlN3cExkMWxDUWtGSFJIWjZRVUpCZDFGdldrUm9hVTVIUlRKT1JGRXhXbXBOTkZsNlVUUlphbXQ0VFhwa2FFOUVRVFZQVkdOM1RtMVJOVmxxWjNkT2VrMTRDazVFV214T1JFRldRbWR2Y2tKblJVVkJXVTh2VFVGRlJVSkJaSGxhVjNoc1dWaE9iRTFEVVVkRGFYTkhRVkZSUW1jM09IZEJVVlZGUm01U2VWbFhiSE1LWWpKYWFXRllVbnBNTTBwdFdYcG5NMDlFVlhWalNHdDNTR2RaUzB0M1dVSkNRVWRFZG5wQlFrSm5VVkZqYlZadFkzazVNRmxYWkhwTU0xbDNUR3BGZFFwTmFrRTNRbWR2Y2tKblJVVkJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveENtTXlWbmxaTWpsMVpFZFdkV1JETldwaU1qQjNZVUZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbUZFUm1odlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5Xb0tZakl3ZG1SSVNtaGhWM2gyV20xS2NHUklUWFpqYlZwcVQwUmpORTVUTlhkbFV6aDFXakpzTUdGSVZtbE1NMlIyWTIxMGJXSkhPVE5qZVRsNVdsZDRiQXBaV0U1c1RHNXNkR0pGUW5sYVYxcDZURE5TYUZvelRYWmtha0YxVFZNMGVVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVXOUZTMmQzYjFwRWFHbE9SMFV5Q2s1RVVURmFhazAwV1hwUk5GbHFhM2hOZW1Sb1QwUkJOVTlVWTNkT2JWRTFXV3BuZDA1NlRYaE9SRnBzVGtSQlpFSm5iM0pDWjBWRlFWbFBMMDFCUlV3S1FrRTRUVVJYWkhCa1IyZ3hXV2t4YjJJelRqQmFWMUYzVDFGWlMwdDNXVUpDUVVkRWRucEJRa1JCVVhKRVEyeHZaRWhTZDJONmIzWk1NbVJ3WkVkb01RcFphVFZxWWpJd2RtUklTbWhoVjNoMldtMUtjR1JJVFhaamJWcHFUMFJqTkU1VE5YZGxWRUUwUW1kdmNrSm5SVVZCV1U4dlRVRkZUa0pEYjAxTFIxRTBDbGxxVW1oT2FsRXdUbGRaZWs5SFRUQlBSMGsxVFZSTk0xbFVaM2RQVkdzelRVUmFhMDlYU1RSTlJHTjZUVlJSTWxwVVVYZEpRVmxMUzNkWlFrSkJSMFFLZG5wQlFrUm5VVk5FUWtKNVdsZGFla3d6VW1oYU0wMTJaR3BCZFUxVE5IbE5RbXRIUTJselIwRlJVVUpuTnpoM1FWRTRSVU4zZDBwT2VsazBUV3BGZWdwUFZHc3pUVU0wUjBOcGMwZEJVVkZDWnpjNGQwRlNRVVZKUVhkbFlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcENtRllVbnBOUW1OSFEybHpSMEZSVVVKbk56aDNRVkpGUlVOUmQwaE5hazE0VGtSUmVVMTZRbTlDWjI5eVFtZEZSVUZaVHk5TlFVVlRRa1p2VFZkSGFEQUtaRWhDZWs5cE9IWmFNbXd3WVVoV2FVeHRUblppVXprd1kyMUdjR0pIT1cxWmJXd3dZM2s1ZVZwdFRUUk9lbWN4VEc1Q05VeDVOVzVoV0ZKdlpGZEpkZ3BrTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFprUjBadVkzazVNazFETkhoTWFrbDNUMEZaUzB0M1dVSkNRVWRFQ25aNlFVSkZkMUZ4UkVOb2EwOUhTVEJaVkZrd1RrUldiVTE2YUdwT1JHaHBUMVJGZWs0eVJUUk5SR3MxVG5wQk1scEViR2xQUkVFelRYcEZNRTV0VlRBS1RVSmpSME5wYzBkQlVWRkNaemM0ZDBGU1VVVkRVWGRJWTIxV2MxcFhSbnBhVkVKalFtZHZja0puUlVWQldVOHZUVUZGVmtKRk5FMVVSMmd3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVNR050Um5CaVJ6bHRXVzFzTUdONU9YbGFiVTAwVG5wbk1VeHVRalZNTWtacVpFZHNkbUp1VFhaamJsWjFDbU41T0RSTmVsVjRUVVJWTkU1VVFYaE1Na1l3WkVkV2RHTklVbnBNZWtWM1JtZFpTMHQzV1VKQ1FVZEVkbnBCUWtablVVbEVRVnAzWkZkS2MyRlhUWGNLWjFsdlIwTnBjMGRCVVZGQ01XNXJRMEpCU1VWbVFWSTJRVWhuUVdSblJHUlFWRUp4ZUhOalVrMXRUVnBJYUhsYVducGpRMjlyY0dWMVRqUTRjbVlyU0FwcGJrdEJUSGx1ZFdwblFVRkJXVFZaTkVWTEswRkJRVVZCZDBKSVRVVlZRMGxFWVdkbWFuQjNNVUZhV0RNM05IWkdXRWRFVTFwblNqbExjWEpqY1RkVUNtc3ZWWE16WmpkdWJWWlJNVUZwUlVFMFpYTkhRbkpFYUdac1lrbFZkV3BWYlZsRE0yVlZWMFpHUW1kWVNHWkJRa3hwVTBSM1kybFVVWGM0ZDBObldVa0tTMjlhU1hwcU1FVkJkMDFFWVZGQmQxcG5TWGhCVFRablMwazFka3R2Y1dOMlZHdDJPRGRHYjNFelYxaE9XVzFCYUZCcU0zRmhVVFZ2WTFoUldITlhlZ3BJWlU1WFIwSTJiRk5JVkVjelJVNTVZWEJ4V1VKblNYaEJUVXBYT1d4NU0wcFlSV1JKTlhsa1NHWjZLMGRhYjJneGEzbGpNRmhHVlZCd05GWTBhMVpxQ201VldGa3JTM1J2VVZkTFUxQklZVnBOYTFsREwzTjZXR2huUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"xOkunsyCi+8qp9uh3orJg1EfdTKg3xHHcNOQmaJc8gE=\"\n        },\n        \"signature\": \"MEYCIQCeH6E3MpZngWA6RPg8HAl/Z764hTFYycNyF3R+mPTSbAIhAPgMS8qBM8lCEU2XW765myMMz2zuyu9iTF40PH+XZlJQ\"\n    }\n}\n"
  },
  {
    "path": "test/assets/bundle_v3_no_signed_time.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is the input for bundle_v3_no_signed_time, which ensures clients reject\nbundles that don't have a source of signed time.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/bundle_v3_no_signed_time.txt.sigstore.json",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle.v0.3+json\",\n    \"verificationMaterial\": {\n        \"certificate\": {\n            \"rawBytes\": \"MIIC1DCCAlugAwIBAgIUXgKINnY7rbT5gHmj9yeiZXGg3rkwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMjEwMjE0MTI1WhcNMjQxMjEwMjE1MTI1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4ul4I08UFGizCla6qRUGFiwEPNsFRnvBPDvQ4ViJ+Q83HOlYWWxCAjoJpGd9FWtyxTPKDsG0n4t6Mr+jSwz22KOCAXowggF2MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUZ7cNLqQlnKAXnf6jmb9cv70dppgwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABk7KFEeIAAAQDAEgwRgIhAOeS6rR2aksHhN9Rxbx+ANuAlXhP4vTPKMLBHd6JAm4lAiEAx+/kzKJ2SxSCAYm582jKeAa1LCVmUaO85FO2WTV7MYEwCgYIKoZIzj0EAwMDZwAwZAIwDXrVAPgutWZWPfE3QWy/4gG/PbMbYUfqNsEpQEeMm8GeraZN3zffzw16FFhWsMbXAjApxDNgKvmztHOKStyvmOXPiJCixzx/gLFbhVn7Q+qY6vjC83B0XgPsyQ2T0i8Ldzg=\"\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"154562758\",\n                \"logId\": {\n                    \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1733866885\",\n                \"inclusionProof\": {\n                    \"logIndex\": \"32658496\",\n                    \"rootHash\": \"IbC2+n9aYhFlm5nFwkp+j7/Hc9XuYWxyE5OlXIoIijY=\",\n                    \"treeSize\": \"32658497\",\n                    \"hashes\": [\n                        \"CVvwGSdkZ5FUDnltf3Me3nXyco4G9mwTsYbIxz0RS+U=\",\n                        \"DJrEpKAKhEPhZ5aKvlaRImFebTv5tc17rsfOkhSS6fY=\",\n                        \"tsYfO+hUsl4KKY+qsPx/k4NzOzE5zWRsc4Ufgn4oh/U=\",\n                        \"ZjSpDQt5kIQfJd6B/BDNWLRhYOGwnlxE6pT4JJaiD5s=\",\n                        \"OMoiMVnwD3sG6Cc6HCg+ySmqBAH1nn0mA5+tjFxiyeg=\",\n                        \"gSWKL2k1ZGZm45C8hSdNwWan8qOrszl5X7Ws56h+FVM=\",\n                        \"R7hO1X+KgSw8Oojd8i2+G3BzBYztkRBE6LpYSXPg33U=\",\n                        \"oOecFfN3YqDOkbijS/ej1WF5Da/Gt/AZNhbwE9uoOE8=\",\n                        \"4lUF0YOu9XkIDXKXA0wMSzd6VeDY3TZAgmoOeWmS2+Y=\",\n                        \"gf+9m552B3PnkWnO0o4KdVvjcT3WVHLrCbf1DoVYKFw=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstore.dev - 1193050959916656506\\n32658497\\nIbC2+n9aYhFlm5nFwkp+j7/Hc9XuYWxyE5OlXIoIijY=\\n\\n\\u2014 rekor.sigstore.dev wNI9ajBGAiEAgjFaCZlVvHUnDgxLf+4XjN6ahWNkkKh9QFTOqHBpyw4CIQDmy4JQs+2BKtvheo/HQogyhh5EYGYZeBDdRvyyX1fg+w==\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjYTZkZTk5YTExZDNkMzgwNTZkODM4YzdkYzlhMjNhMTFhMGM4MWJjYWNlMGQxMWVhYTMwMWEyZmZiNDgyYzQyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUMzc2pYZVZoTHRqbE13dG0yRE5CYVdVaFBWOVJ1U1dsWW1EcHQzRzFQVW5RSWhBUElxRHUwTVkza1FtelE2QmswS2VSTW5mQ3Y0VVdEVU5jclRnN0cyYjdzTCIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4MVowRjNTVUpCWjBsVldHZExTVTV1V1RkeVlsUTFaMGh0YWpsNVpXbGFXRWRuTTNKcmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFxUlhkTmFrVXdUVlJKTVZkb1kwNU5hbEY0VFdwRmQwMXFSVEZOVkVreFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUwZFd3MFNUQTRWVVpIYVhwRGJHRTJjVkpWUjBacGQwVlFUbk5HVW01MlFsQkVkbEVLTkZacFNpdFJPRE5JVDJ4WlYxZDRRMEZxYjBwd1IyUTVSbGQwZVhoVVVFdEVjMGN3YmpSME5rMXlLMnBUZDNveU1rdFBRMEZZYjNkblowWXlUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZhTjJOT0NreHhVV3h1UzBGWWJtWTJhbTFpT1dOMk56QmtjSEJuZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwZDFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpsQ1NITkJaVkZDTTBGT01EbE5SM0pIZUhoRmVWbDRhMlZJU214dVRuZExhVk5zTmpRemFubDBMelJsUzJOdlFYWkxaVFpQUVVGQlFncHJOMHRHUldWSlFVRkJVVVJCUldkM1VtZEphRUZQWlZNMmNsSXlZV3R6U0doT09WSjRZbmdyUVU1MVFXeFlhRkEwZGxSUVMwMU1Ra2hrTmtwQmJUUnNDa0ZwUlVGNEt5OXJla3RLTWxONFUwTkJXVzAxT0RKcVMyVkJZVEZNUTFadFZXRlBPRFZHVHpKWFZGWTNUVmxGZDBObldVbExiMXBKZW1vd1JVRjNUVVFLV25kQmQxcEJTWGRFV0hKV1FWQm5kWFJYV2xkUVprVXpVVmQ1THpSblJ5OVFZazFpV1ZWbWNVNXpSWEJSUldWTmJUaEhaWEpoV2s0emVtWm1lbmN4TmdwR1JtaFhjMDFpV0VGcVFYQjRSRTVuUzNadGVuUklUMHRUZEhsMmJVOVlVR2xLUTJsNGVuZ3ZaMHhHWW1oV2JqZFJLM0ZaTm5acVF6Z3pRakJZWjFCekNubFJNbFF3YVRoTVpIcG5QUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=\"\n            }\n        ],\n        \"timestampVerificationData\": {}\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"ym3pmhHT04BW2DjH3JojoRoMgbys4NEeqjAaL/tILEI=\"\n        },\n        \"signature\": \"MEYCIQC3sjXeVhLtjlMwtm2DNBaWUhPV9RuSWlYmDpt3G1PUnQIhAPIqDu0MY3kQmzQ6Bk0KeRMnfCv4UWDUNcrTg7G2b7sL\"\n    }\n}\n"
  },
  {
    "path": "test/assets/c.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"c.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/c.txt.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDwTCCA0igAwIBAgIUdXPCI40ren/SEkqxmHcCc6lIV7MwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjIxMTAzMDc0NTM1WhcNMjIxMTAzMDc1NTM1WjAAMHYwEAYH\nKoZIzj0CAQYFK4EEACIDYgAELVUlqi4FjTw4mHzuyE8sOsK6mVvzOTv0EX7ot+aZ\nftaf+ato9xuemqA69qARscFPwG15It1F9PVdKUOeJkTPjZC+lRHNAIeamJpilskz\nxqR6fisI7q72zHY8OhgMnSSHo4ICSjCCAkYwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud\nJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBREb7Dfm1g8gILV3K9rT9WSF7GnzzAf\nBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDBmBgNVHREBAf8EXDBahlho\ndHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtcHl0aG9uLy5naXRo\ndWIvd29ya2Zsb3dzL2NpLnltbEByZWZzL3B1bGwvMjg4L21lcmdlMDkGCisGAQQB\ng78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5j\nb20wGgYKKwYBBAGDvzABAgQMcHVsbF9yZXF1ZXN0MDYGCisGAQQBg78wAQMEKDNi\nZTcyMzU2ZWY0NTE3YmI4ZTUwZjI5Njg4N2Y5YzU3ODZmOTAzMTYwEAYKKwYBBAGD\nvzABBAQCQ0kwJgYKKwYBBAGDvzABBQQYc2lnc3RvcmUvc2lnc3RvcmUtcHl0aG9u\nMCEGCisGAQQBg78wAQYEE3JlZnMvcHVsbC8yODgvbWVyZ2UwgYoGCisGAQQB1nkC\nBAIEfAR6AHgAdgArMLzcaIjJ4uHYJiledB9IOTGWAvKcM8teQ0D+sqyGegAAAYQ8\nc9igAAAEAwBHMEUCIQCn/JSbLxs0ds3Nycn0yINUQABeltbAmcYDFEn/sdm50gIg\nfm4lKdhXJoWHJRC8IS7MxYI3yR/oNzX6dntuqpHJ24YwCgYIKoZIzj0EAwMDZwAw\nZAIwE0F3B/HgHn+ov6axOY0TMR/hv2DUVlC3qkGBQEEMtglf5qtT+a9g7aQ5g4pG\nof+JAjB+qUeUdSAyGPDK+5Ti6aROy0oAbwl+B3bH7QmmZ/i5M++PXIW4l4lcuAmA\nUkjTgLw=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/c.txt.sig",
    "content": "MGUCMAQYRaYOdZEOT3C3WP22sC9+2euiFGYbC4VNefWVL31+MAL7oKMWsHsBwh1ngjTZHAIxALuUf+mzlACBqYUSTTwl3LFIGUGl8g3Z6wkTMsqdI1NrtHj0rVpcWA1DIO4GhGOM5w==\n"
  },
  {
    "path": "test/assets/integration/Python-3.12.5.tgz.sigstore",
    "content": "{\"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==\"}}\n"
  },
  {
    "path": "test/assets/integration/a.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"a.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/integration/attest/slsa_predicate_v0_2.json",
    "content": "{\n    \"builder\": {\n        \"id\": \"https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.0.0\"\n    },\n    \"buildType\": \"https://github.com/slsa-framework/slsa-github-generator/generic@v1\",\n    \"invocation\": {\n        \"configSource\": {\n            \"uri\": \"git+https://github.com/sigstore/sigstore-python@refs/tags/v3.2.0\",\n            \"digest\": {\n                \"sha1\": \"fc29ec190575ae345cea23f0953b64ca6f2ab8ba\"\n            },\n            \"entryPoint\": \".github/workflows/release.yml\"\n        },\n        \"parameters\": {},\n        \"environment\": {\n            \"github_actor\": \"woodruffw\",\n            \"github_actor_id\": \"3059210\",\n            \"github_base_ref\": \"\",\n            \"github_event_name\": \"release\",\n            \"github_event_payload\": {\n                \"action\": \"published\",\n                \"enterprise\": {\n                    \"avatar_url\": \"https://avatars.githubusercontent.com/b/102459?v=4\",\n                    \"created_at\": \"2023-12-08T05:54:26Z\",\n                    \"description\": \"Open Source Security Foundation (OpenSSF)\",\n                    \"html_url\": \"https://github.com/enterprises/openssf\",\n                    \"id\": 102459,\n                    \"name\": \"Open Source Security Foundation\",\n                    \"node_id\": \"E_kgDOAAGQOw\",\n                    \"slug\": \"openssf\",\n                    \"updated_at\": \"2024-01-06T00:47:02Z\",\n                    \"website_url\": \"https://openssf.org/\"\n                },\n                \"organization\": {\n                    \"avatar_url\": \"https://avatars.githubusercontent.com/u/71096353?v=4\",\n                    \"description\": \"Software Supply Chain Security\",\n                    \"events_url\": \"https://api.github.com/orgs/sigstore/events\",\n                    \"hooks_url\": \"https://api.github.com/orgs/sigstore/hooks\",\n                    \"id\": 71096353,\n                    \"issues_url\": \"https://api.github.com/orgs/sigstore/issues\",\n                    \"login\": \"sigstore\",\n                    \"members_url\": \"https://api.github.com/orgs/sigstore/members{/member}\",\n                    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjcxMDk2MzUz\",\n                    \"public_members_url\": \"https://api.github.com/orgs/sigstore/public_members{/member}\",\n                    \"repos_url\": \"https://api.github.com/orgs/sigstore/repos\",\n                    \"url\": \"https://api.github.com/orgs/sigstore\"\n                },\n                \"release\": {\n                    \"assets\": [],\n                    \"assets_url\": \"https://api.github.com/repos/sigstore/sigstore-python/releases/170913493/assets\",\n                    \"author\": {\n                        \"avatar_url\": \"https://avatars.githubusercontent.com/u/3059210?v=4\",\n                        \"events_url\": \"https://api.github.com/users/woodruffw/events{/privacy}\",\n                        \"followers_url\": \"https://api.github.com/users/woodruffw/followers\",\n                        \"following_url\": \"https://api.github.com/users/woodruffw/following{/other_user}\",\n                        \"gists_url\": \"https://api.github.com/users/woodruffw/gists{/gist_id}\",\n                        \"gravatar_id\": \"\",\n                        \"html_url\": \"https://github.com/woodruffw\",\n                        \"id\": 3059210,\n                        \"login\": \"woodruffw\",\n                        \"node_id\": \"MDQ6VXNlcjMwNTkyMTA=\",\n                        \"organizations_url\": \"https://api.github.com/users/woodruffw/orgs\",\n                        \"received_events_url\": \"https://api.github.com/users/woodruffw/received_events\",\n                        \"repos_url\": \"https://api.github.com/users/woodruffw/repos\",\n                        \"site_admin\": false,\n                        \"starred_url\": \"https://api.github.com/users/woodruffw/starred{/owner}{/repo}\",\n                        \"subscriptions_url\": \"https://api.github.com/users/woodruffw/subscriptions\",\n                        \"type\": \"User\",\n                        \"url\": \"https://api.github.com/users/woodruffw\"\n                    },\n                    \"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\",\n                    \"created_at\": \"2024-08-19T17:14:19Z\",\n                    \"draft\": false,\n                    \"html_url\": \"https://github.com/sigstore/sigstore-python/releases/tag/v3.2.0\",\n                    \"id\": 170913493,\n                    \"name\": \"v3.2.0\",\n                    \"node_id\": \"RE_kwDOGq85Ts4KL-7V\",\n                    \"prerelease\": false,\n                    \"published_at\": \"2024-08-19T17:15:11Z\",\n                    \"tag_name\": \"v3.2.0\",\n                    \"tarball_url\": \"https://api.github.com/repos/sigstore/sigstore-python/tarball/v3.2.0\",\n                    \"target_commitish\": \"main\",\n                    \"upload_url\": \"https://uploads.github.com/repos/sigstore/sigstore-python/releases/170913493/assets{?name,label}\",\n                    \"url\": \"https://api.github.com/repos/sigstore/sigstore-python/releases/170913493\",\n                    \"zipball_url\": \"https://api.github.com/repos/sigstore/sigstore-python/zipball/v3.2.0\"\n                },\n                \"repository\": {\n                    \"allow_forking\": true,\n                    \"archive_url\": \"https://api.github.com/repos/sigstore/sigstore-python/{archive_format}{/ref}\",\n                    \"archived\": false,\n                    \"assignees_url\": \"https://api.github.com/repos/sigstore/sigstore-python/assignees{/user}\",\n                    \"blobs_url\": \"https://api.github.com/repos/sigstore/sigstore-python/git/blobs{/sha}\",\n                    \"branches_url\": \"https://api.github.com/repos/sigstore/sigstore-python/branches{/branch}\",\n                    \"clone_url\": \"https://github.com/sigstore/sigstore-python.git\",\n                    \"collaborators_url\": \"https://api.github.com/repos/sigstore/sigstore-python/collaborators{/collaborator}\",\n                    \"comments_url\": \"https://api.github.com/repos/sigstore/sigstore-python/comments{/number}\",\n                    \"commits_url\": \"https://api.github.com/repos/sigstore/sigstore-python/commits{/sha}\",\n                    \"compare_url\": \"https://api.github.com/repos/sigstore/sigstore-python/compare/{base}...{head}\",\n                    \"contents_url\": \"https://api.github.com/repos/sigstore/sigstore-python/contents/{+path}\",\n                    \"contributors_url\": \"https://api.github.com/repos/sigstore/sigstore-python/contributors\",\n                    \"created_at\": \"2022-01-13T17:29:37Z\",\n                    \"custom_properties\": {},\n                    \"default_branch\": \"main\",\n                    \"deployments_url\": \"https://api.github.com/repos/sigstore/sigstore-python/deployments\",\n                    \"description\": \"A Sigstore client written in Python\",\n                    \"disabled\": false,\n                    \"downloads_url\": \"https://api.github.com/repos/sigstore/sigstore-python/downloads\",\n                    \"events_url\": \"https://api.github.com/repos/sigstore/sigstore-python/events\",\n                    \"fork\": false,\n                    \"forks\": 41,\n                    \"forks_count\": 41,\n                    \"forks_url\": \"https://api.github.com/repos/sigstore/sigstore-python/forks\",\n                    \"full_name\": \"sigstore/sigstore-python\",\n                    \"git_commits_url\": \"https://api.github.com/repos/sigstore/sigstore-python/git/commits{/sha}\",\n                    \"git_refs_url\": \"https://api.github.com/repos/sigstore/sigstore-python/git/refs{/sha}\",\n                    \"git_tags_url\": \"https://api.github.com/repos/sigstore/sigstore-python/git/tags{/sha}\",\n                    \"git_url\": \"git://github.com/sigstore/sigstore-python.git\",\n                    \"has_discussions\": false,\n                    \"has_downloads\": true,\n                    \"has_issues\": true,\n                    \"has_pages\": true,\n                    \"has_projects\": true,\n                    \"has_wiki\": false,\n                    \"homepage\": \"https://pypi.org/p/sigstore\",\n                    \"hooks_url\": \"https://api.github.com/repos/sigstore/sigstore-python/hooks\",\n                    \"html_url\": \"https://github.com/sigstore/sigstore-python\",\n                    \"id\": 447691086,\n                    \"is_template\": false,\n                    \"issue_comment_url\": \"https://api.github.com/repos/sigstore/sigstore-python/issues/comments{/number}\",\n                    \"issue_events_url\": \"https://api.github.com/repos/sigstore/sigstore-python/issues/events{/number}\",\n                    \"issues_url\": \"https://api.github.com/repos/sigstore/sigstore-python/issues{/number}\",\n                    \"keys_url\": \"https://api.github.com/repos/sigstore/sigstore-python/keys{/key_id}\",\n                    \"labels_url\": \"https://api.github.com/repos/sigstore/sigstore-python/labels{/name}\",\n                    \"language\": \"Python\",\n                    \"languages_url\": \"https://api.github.com/repos/sigstore/sigstore-python/languages\",\n                    \"license\": {\n                        \"key\": \"other\",\n                        \"name\": \"Other\",\n                        \"node_id\": \"MDc6TGljZW5zZTA=\",\n                        \"spdx_id\": \"NOASSERTION\",\n                        \"url\": null\n                    },\n                    \"merges_url\": \"https://api.github.com/repos/sigstore/sigstore-python/merges\",\n                    \"milestones_url\": \"https://api.github.com/repos/sigstore/sigstore-python/milestones{/number}\",\n                    \"mirror_url\": null,\n                    \"name\": \"sigstore-python\",\n                    \"node_id\": \"R_kgDOGq85Tg\",\n                    \"notifications_url\": \"https://api.github.com/repos/sigstore/sigstore-python/notifications{?since,all,participating}\",\n                    \"open_issues\": 28,\n                    \"open_issues_count\": 28,\n                    \"owner\": {\n                        \"avatar_url\": \"https://avatars.githubusercontent.com/u/71096353?v=4\",\n                        \"events_url\": \"https://api.github.com/users/sigstore/events{/privacy}\",\n                        \"followers_url\": \"https://api.github.com/users/sigstore/followers\",\n                        \"following_url\": \"https://api.github.com/users/sigstore/following{/other_user}\",\n                        \"gists_url\": \"https://api.github.com/users/sigstore/gists{/gist_id}\",\n                        \"gravatar_id\": \"\",\n                        \"html_url\": \"https://github.com/sigstore\",\n                        \"id\": 71096353,\n                        \"login\": \"sigstore\",\n                        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjcxMDk2MzUz\",\n                        \"organizations_url\": \"https://api.github.com/users/sigstore/orgs\",\n                        \"received_events_url\": \"https://api.github.com/users/sigstore/received_events\",\n                        \"repos_url\": \"https://api.github.com/users/sigstore/repos\",\n                        \"site_admin\": false,\n                        \"starred_url\": \"https://api.github.com/users/sigstore/starred{/owner}{/repo}\",\n                        \"subscriptions_url\": \"https://api.github.com/users/sigstore/subscriptions\",\n                        \"type\": \"Organization\",\n                        \"url\": \"https://api.github.com/users/sigstore\"\n                    },\n                    \"private\": false,\n                    \"pulls_url\": \"https://api.github.com/repos/sigstore/sigstore-python/pulls{/number}\",\n                    \"pushed_at\": \"2024-08-19T17:14:57Z\",\n                    \"releases_url\": \"https://api.github.com/repos/sigstore/sigstore-python/releases{/id}\",\n                    \"size\": 1835,\n                    \"ssh_url\": \"git@github.com:sigstore/sigstore-python.git\",\n                    \"stargazers_count\": 219,\n                    \"stargazers_url\": \"https://api.github.com/repos/sigstore/sigstore-python/stargazers\",\n                    \"statuses_url\": \"https://api.github.com/repos/sigstore/sigstore-python/statuses/{sha}\",\n                    \"subscribers_url\": \"https://api.github.com/repos/sigstore/sigstore-python/subscribers\",\n                    \"subscription_url\": \"https://api.github.com/repos/sigstore/sigstore-python/subscription\",\n                    \"svn_url\": \"https://github.com/sigstore/sigstore-python\",\n                    \"tags_url\": \"https://api.github.com/repos/sigstore/sigstore-python/tags\",\n                    \"teams_url\": \"https://api.github.com/repos/sigstore/sigstore-python/teams\",\n                    \"topics\": [\n                        \"codesigning\",\n                        \"python\",\n                        \"security\",\n                        \"supply-chain\"\n                    ],\n                    \"trees_url\": \"https://api.github.com/repos/sigstore/sigstore-python/git/trees{/sha}\",\n                    \"updated_at\": \"2024-08-19T17:14:23Z\",\n                    \"url\": \"https://api.github.com/repos/sigstore/sigstore-python\",\n                    \"visibility\": \"public\",\n                    \"watchers\": 219,\n                    \"watchers_count\": 219,\n                    \"web_commit_signoff_required\": true\n                },\n                \"sender\": {\n                    \"avatar_url\": \"https://avatars.githubusercontent.com/u/3059210?v=4\",\n                    \"events_url\": \"https://api.github.com/users/woodruffw/events{/privacy}\",\n                    \"followers_url\": \"https://api.github.com/users/woodruffw/followers\",\n                    \"following_url\": \"https://api.github.com/users/woodruffw/following{/other_user}\",\n                    \"gists_url\": \"https://api.github.com/users/woodruffw/gists{/gist_id}\",\n                    \"gravatar_id\": \"\",\n                    \"html_url\": \"https://github.com/woodruffw\",\n                    \"id\": 3059210,\n                    \"login\": \"woodruffw\",\n                    \"node_id\": \"MDQ6VXNlcjMwNTkyMTA=\",\n                    \"organizations_url\": \"https://api.github.com/users/woodruffw/orgs\",\n                    \"received_events_url\": \"https://api.github.com/users/woodruffw/received_events\",\n                    \"repos_url\": \"https://api.github.com/users/woodruffw/repos\",\n                    \"site_admin\": false,\n                    \"starred_url\": \"https://api.github.com/users/woodruffw/starred{/owner}{/repo}\",\n                    \"subscriptions_url\": \"https://api.github.com/users/woodruffw/subscriptions\",\n                    \"type\": \"User\",\n                    \"url\": \"https://api.github.com/users/woodruffw\"\n                }\n            },\n            \"github_head_ref\": \"\",\n            \"github_ref\": \"refs/tags/v3.2.0\",\n            \"github_ref_type\": \"tag\",\n            \"github_repository_id\": \"447691086\",\n            \"github_repository_owner\": \"sigstore\",\n            \"github_repository_owner_id\": \"71096353\",\n            \"github_run_attempt\": \"1\",\n            \"github_run_id\": \"10457864437\",\n            \"github_run_number\": \"61\",\n            \"github_sha1\": \"fc29ec190575ae345cea23f0953b64ca6f2ab8ba\"\n        }\n    },\n    \"metadata\": {\n        \"buildInvocationId\": \"10457864437-1\",\n        \"completeness\": {\n            \"parameters\": true,\n            \"environment\": false,\n            \"materials\": false\n        },\n        \"reproducible\": false\n    },\n    \"materials\": [\n        {\n            \"uri\": \"git+https://github.com/sigstore/sigstore-python@refs/tags/v3.2.0\",\n            \"digest\": {\n                \"sha1\": \"fc29ec190575ae345cea23f0953b64ca6f2ab8ba\"\n            }\n        }\n    ]\n}"
  },
  {
    "path": "test/assets/integration/attest/slsa_predicate_v1_0.json",
    "content": "{\n    \"buildDefinition\": {\n        \"buildType\": \"https://actions.github.io/buildtypes/workflow/v1\",\n        \"externalParameters\": {\n            \"workflow\": {\n                \"ref\": \"refs/tags/1.21.0\",\n                \"repository\": \"https://github.com/octo-org/octo-repo\",\n                \"path\": \".github/workflows/ci.yaml\"\n            }\n        },\n        \"internalParameters\": {\n            \"github\": {\n                \"event_name\": \"push\",\n                \"repository_id\": \"000000000\",\n                \"repository_owner_id\": \"0000000\",\n                \"runner_environment\": \"github-hosted\"\n            }\n        },\n        \"resolvedDependencies\": [\n            {\n                \"uri\": \"git+https://github.com/octo-org/octo-repo@refs/tags/1.21.0\",\n                \"digest\": {\n                    \"gitCommit\": \"1ac93ce21ee526b36fd154b9058d97dfaa424c50\"\n                }\n            }\n        ]\n    },\n    \"runDetails\": {\n        \"builder\": {\n            \"id\": \"https://github.com/octo-org/octo-repo/.github/workflows/docker.yaml@refs/heads/development\"\n        },\n        \"metadata\": {\n            \"invocationId\": \"https://github.com/octo-org/octo-repo/actions/runs/10313983218/attempts/2\"\n        }\n    }\n}"
  },
  {
    "path": "test/assets/integration/b.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"b.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/integration/bundle_v3.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is the input for bundle_v3, which tests support for \"v3\" bundles.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/integration/bundle_v3.txt.sigstore",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.bundle.v0.3+json\",\n    \"verificationMaterial\": {\n        \"certificate\": {\n            \"rawBytes\": \"MIIC1DCCAlqgAwIBAgIUO3tlVbLtvLPp+6zGOtep1SPkRigwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDAyMTkxOTA5WhcNMjQwNDAyMTkyOTA5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENdrfpgNU1Rjmz+j65rpJWKc08ruKYy4FX7nmmOnbauFZimsQXrdyDSXKNRtEXX4X3t/Amt+euwPDBh+eq7BCnqOCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUGRlBhD0wvzAfLb2dMWOgPrrJuRkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjqBAQZ4AAAQDAEcwRQIgeWUmtnD0MFUl5kkX7nbMdLWCsDGIPzdIlN+WaZF0TmkCIQC7+31saqrFe9RmduVZ2dxXhUPrajltuSDHb1vSGOcuHjAKBggqhkjOPQQDAwNoADBlAjEAn2+uuLHsnH9Db7zkIdF65YhiXbgMMF//iHc+B/QETK0HYVcOPTK3p46FUzXFD6xrAjAO2hrkfjBKANKjJJxHV3FVrtS+TR0GCP0HzC3D7Br95TXzfO7+j4Dd8/N/aAr6Ibs=\"\n        },\n        \"tlogEntries\": [\n            {\n                \"logIndex\": \"25915956\",\n                \"logId\": {\n                    \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n                },\n                \"kindVersion\": {\n                    \"kind\": \"hashedrekord\",\n                    \"version\": \"0.0.1\"\n                },\n                \"integratedTime\": \"1712085549\",\n                \"inclusionPromise\": {\n                    \"signedEntryTimestamp\": \"MEYCIQD2KXW1NppUhkPPzGR8NrUIyN+MzZSSqGZQO7CzvhSnYgIhAO9AHzjbsr1AHXRHmEpdPZcoFHEwwMTgfqwjoOXVMmqN\"\n                },\n                \"inclusionProof\": {\n                    \"logIndex\": \"25901137\",\n                    \"rootHash\": \"iGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=\",\n                    \"treeSize\": \"25901138\",\n                    \"hashes\": [\n                        \"UHUr+lvxENI+G902oEsFW5ovQILgqO9mUWWxvvwHZZc=\",\n                        \"IcMBsbH3GRW8FX2CiL/ljMb45vzmENmhp5Yp/7IW998=\",\n                        \"SxC6nr0zP+a6kWb6nO2fmEtz8BYAbqEXc+dsqGLdRPM=\",\n                        \"sppZRSz/vdeLlavgvICrXHLeReMTJw98bs9HJ0I8WnE=\",\n                        \"c8lCSuBS6MzrRnt6OiyYjqhTyxUI/22gpVB7dblfDis=\",\n                        \"eJk64J6cMpIljPSX/72kH0kiIeElyypQm5vJ2gMMyHw=\",\n                        \"hbIK+jmAwQjU7Yi3iKvnfR1u7GNippk7QsRwJXIuRaw=\",\n                        \"tpHWIEB2vNU5ZmC68dj1Hh9cwQK083ozogA6zJ3cJ8A=\",\n                        \"arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=\",\n                        \"Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=\",\n                        \"rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=\"\n                    ],\n                    \"checkpoint\": {\n                        \"envelope\": \"rekor.sigstage.dev - 8050909264565447525\\n25901138\\niGAoHccJIyFemFxmEftti2YC8hvPqixBi5y1EyvfF4c=\\n\\n\\u2014 rekor.sigstage.dev 0y8wozBFAiAMJJLbnNOnmizMbVBz9/A/qnMK15BudWoZkuE+obD6CAIhAJf6A3h2iOpuhz/duEhG3fbAQG9PXln4wXPHFBT5wT1a\\n\"\n                    }\n                },\n                \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ZTZhZTlkZTU4YzExNzdiZWE2MTViNGZjYmZiMmZkNjg4ZThjNGI1MWMyZTU2YjZhMzhlODE3ODMzZWMyNGEyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRFFTSmk5YWVydFFobVQrY2UxaktOZENlNEtTY3NLR3E5ZlBtMzQyMkRCU0FpRUFoajFzeFo5Nm9ySVRzUXh5TUxJRFJKaW1wb3kxSjFNeWZsY1FWd2tremhzPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhSRU5EUVd4eFowRjNTVUpCWjBsVlR6TjBiRlppVEhSMlRGQndLelo2UjA5MFpYQXhVMUJyVW1sbmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVhsTlZHdDRUMVJCTlZkb1kwNU5hbEYzVGtSQmVVMVVhM2xQVkVFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZPWkhKbWNHZE9WVEZTYW0xNksybzJOWEp3U2xkTFl6QTRjblZMV1hrMFJsZzNibTBLYlU5dVltRjFSbHBwYlhOUldISmtlVVJUV0V0T1VuUkZXRmcwV0ROMEwwRnRkQ3RsZFhkUVJFSm9LMlZ4TjBKRGJuRlBRMEZZYTNkblowWXhUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIVW14Q0NtaEVNSGQyZWtGbVRHSXlaRTFYVDJkUWNuSktkVkpyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwWjFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpoQ1NHOUJaVUZDTWtGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFjVUpCVVZvMFFVRkJVVVJCUldOM1VsRkpaMlZYVlcxMGJrUXdUVVpWYkRWcmExZzNibUpOWkV4WFEzTkVSMGxRZW1SSmJFNHJWMkZhUmpCVWJXdERDa2xSUXpjck16RnpZWEZ5Um1VNVVtMWtkVlphTW1SNFdHaFZVSEpoYW14MGRWTkVTR0l4ZGxOSFQyTjFTR3BCUzBKblozRm9hMnBQVUZGUlJFRjNUbThLUVVSQ2JFRnFSVUZ1TWl0MWRVeEljMjVJT1VSaU4zcHJTV1JHTmpWWmFHbFlZbWROVFVZdkwybElZeXRDTDFGRlZFc3dTRmxXWTA5UVZFc3pjRFEyUmdwVmVsaEdSRFo0Y2tGcVFVOHlhSEpyWm1wQ1MwRk9TMnBLU25oSVZqTkdWbkowVXl0VVVqQkhRMUF3U0hwRE0wUTNRbkk1TlZSWWVtWlBOeXRxTkVSa0NqZ3ZUaTloUVhJMlNXSnpQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=\"\n            }\n        ]\n    },\n    \"messageSignature\": {\n        \"messageDigest\": {\n            \"algorithm\": \"SHA2_256\",\n            \"digest\": \"Xmrp3ljBF3vqYVtPy/sv1ojoxLUcLla2o46BeDPsJKI=\"\n        },\n        \"signature\": \"MEUCIDQSJi9aertQhmT+ce1jKNdCe4KScsKGq9fPm3422DBSAiEAhj1sxZ96orITsQxyMLIDRJimpoy1J1MyflcQVwkkzhs=\"\n    }\n}\n"
  },
  {
    "path": "test/assets/integration/c.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"c.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/offline-rekor.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"offline-rekor.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/offline-rekor.txt.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEfzCCBAWgAwIBAgIULV3qds9Z1Ar1hOpW+/9ULyl1LgwwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjIxMDEzMTk0ODMyWhcNMjIxMDEzMTk1ODMyWjAAMHYwEAYH\nKoZIzj0CAQYFK4EEACIDYgAEff7HebXAkCGKe8/QMmJ3OCjSOhsR+3NGYn1FKm7R\n672BvHek5Zza2D5bFDEwBEtM3E9hM2//OwN2EU8dK6BAaVGtlEHZvAzCcWCUwWFj\n8QTp9eQDt3Hrmygyp9qB6mOro4IDBzCCAwMwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud\nJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTxc4F9+1z0h4kG410C/f0NxerAhzAf\nBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3\naWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRo\ndWIuY29tL2xvZ2luL29hdXRoMIICRwYKKwYBBAHWeQIEAgSCAjcEggIzAjECLwAb\nfBQqTpkrp98eH8V0JFQTbBV6TLMcQQilIbixq+e/TAAAAYPS5C1VAAAEAQIAO9w0\n5StsDZsK27vfjH1nmzhB8dAcifwsCduL7XS079Jz9hUfcjqKMZOQbL5dlulkteqm\noQPO272u/AxLca7gKDD47gBx0/O9yk6TapGQuqsNrn2JPpfMdvzwJvXQJ/7rL61l\nd6zs/3q0UQQu4PqVIdDPhNF9chUMGiau5UKACsManYKtmTi86+wcCT89Etb9SqSj\n+QiTlTzQqIi9cKXbUhOTzpiKALjwNvsvB5pQ6U9WN+8OVoQPr919js+O0AeVf8R6\nYKhVumMBquvV756FocC/lxThYITbmUH91bY/nQPYy4tAhuums6Cc+9vzYaeQw6y0\ndUfum1XM8agJsihYzuaL/U0S2n8HrfsLjLU6a06IPMEx7WVGSEZxTH78PurXDKB8\nsLKG2X2wIQpiyglk6CU0zgw4WXb+qON7VFIL4wOe5tdrSHwRdV6xqGOdeSf+TyH4\n7GRPa0raT2pVWAZf6liJPD4vqH2jJWE3WbhOWkfYM9uqoE1fQSQr7GN4+NJzmsdN\nscxsD2tiExlXNIMIvpXqTrbWSxDC/reMPjnbpNUHBCwqSyaL7HyW0oB3e6JJOuWl\nyFDJIimX2tpLWyMV4tLCMd/p3EZsE5oCs1cGOiDQhAVUTwJOtxH6jk+vhFDJSH6C\ngkyIyu8vQAwVGatCdElYKK6R0kt8/yA9szrsFMwwCgYIKoZIzj0EAwMDaAAwZQIx\nAJDWJS41EwSk8LLZyqBjK2rG77+ceBjD2Vx6h1oGHVGVBwsiq4CgPsEyPJtVW+1Q\n8wIwZ/gMuXAzIllTHJ4HBFTkODEPUcVYctRDkF75V2lvtS4eO0JFc+agbn/Ah99V\naprh\n-----END CERTIFICATE-----\n\n"
  },
  {
    "path": "test/assets/offline-rekor.txt.sig",
    "content": "MGUCMQCkHC+iuvTo9H1E4ygqCvSq+dAxbqO9Grg12GJDlRe0hMO+TdE/cn2KRB7VGonN0EMCMBvtIkjcIcbBSV0H8pPmpsZiH/OxWc5J7jyEJLERq/M71GamZOor9xx5x83L8Dg2HA==\n"
  },
  {
    "path": "test/assets/signing_config/signingconfig-only-v1-rekor.v2.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n  \"caUrls\": [\n    {\n      \"url\": \"https://fulcio.example.com\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2023-04-14T21:38:40Z\"\n      },\n      \"operator\": \"example.com\"\n    },\n    {\n      \"url\": \"https://fulcio-old.example.com\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2022-04-14T21:38:40Z\",\n        \"end\": \"2023-04-14T21:38:40Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"oidcUrls\": [\n    {\n      \"url\": \"https://oauth2.example.com/auth\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-16T00:00:00Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"rekorTlogUrls\": [\n    {\n      \"url\": \"https://rekor.example.com\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2021-01-12T11:53:27Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"tsaUrls\": [\n    {\n      \"url\": \"https://timestamp.example.com/api/v1/timestamp\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-09T00:00:00Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"rekorTlogConfig\": {\n    \"selector\": \"ANY\"\n  },\n  \"tsaConfig\": {\n    \"selector\": \"ANY\"\n  }\n}\n"
  },
  {
    "path": "test/assets/signing_config/signingconfig.v2.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n  \"caUrls\": [\n    {\n      \"url\": \"https://fulcio.example.com\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2023-04-14T21:38:40Z\"\n      },\n      \"operator\": \"example.com\"\n    },\n    {\n      \"url\": \"https://fulcio-old.example.com\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2022-04-14T21:38:40Z\",\n        \"end\": \"2023-04-14T21:38:40Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"oidcUrls\": [\n    {\n      \"url\": \"https://oauth2.example.com/auth\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-16T00:00:00Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"rekorTlogUrls\": [\n    {\n      \"url\": \"https://rekor.example.com\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2021-01-12T11:53:27Z\"\n      },\n      \"operator\": \"example.com\"\n    },\n    {\n      \"url\": \"https://rekor-v2.example.com\",\n      \"majorApiVersion\": 2,\n      \"validFor\": {\n        \"start\": \"2021-01-12T11:53:27Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"tsaUrls\": [\n    {\n      \"url\": \"https://timestamp.example.com/api/v1/timestamp\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-09T00:00:00Z\"\n      },\n      \"operator\": \"example.com\"\n    }\n  ],\n  \"rekorTlogConfig\": {\n    \"selector\": \"ANY\"\n  },\n  \"tsaConfig\": {\n    \"selector\": \"ANY\"\n  }\n}\n"
  },
  {
    "path": "test/assets/staging-rekor-v2.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"staging-rekor-v2.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/staging-rekor-v2.txt.sigstore.json",
    "content": "{\"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=\"}}\n"
  },
  {
    "path": "test/assets/staging-tuf/16.snapshot.json",
    "content": "{\n \"signatures\": [\n  {\n   \"keyid\": \"c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4\",\n   \"sig\": \"304402202733036a5044a3257392cb6737c80d1972aa2bce8e7194fac23e3d0b939e83ce0220797111c4aa47094278a2997d727c728fcda795b02b8ec803e2265fdac9614a21\"\n  }\n ],\n \"signed\": {\n  \"_type\": \"snapshot\",\n  \"expires\": \"2035-06-11T11:54:57Z\",\n  \"meta\": {\n   \"registry.npmjs.org.json\": {\n    \"version\": 5\n   },\n   \"targets.json\": {\n    \"version\": 17\n   }\n  },\n  \"spec_version\": \"1.0\",\n  \"version\": 16\n }\n}"
  },
  {
    "path": "test/assets/staging-tuf/17.targets.json",
    "content": "{\n \"signatures\": [\n  {\n   \"keyid\": \"aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81\",\n   \"sig\": \"3045022031cbae59944160c1b9b1df859c43cf74d8c5257c32924f1c78146ccd621aae53022100cc8097664966a0f187e41643a61524613434517ec97c9a21f319752fd842e122\"\n  },\n  {\n   \"keyid\": \"61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc\",\n   \"sig\": \"30440220149fb96582721bcaf506b06465cf8df9b4b4c7847f19165eec8f7faeccc61ed8022020090a30e448e7cd71824bf0042ce9982b8882e557be343a919ffc4d825927f6\"\n  },\n  {\n   \"keyid\": \"9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237\",\n   \"sig\": \"\"\n  },\n  {\n   \"keyid\": \"0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5\",\n   \"sig\": \"\"\n  }\n ],\n \"signed\": {\n  \"_type\": \"targets\",\n  \"delegations\": {\n   \"keys\": {\n    \"5e3a4021b11a425fd0a444f1670457ce5b15bbe036144f2417426f7f4b9721da\": {\n     \"keytype\": \"ecdsa\",\n     \"keyval\": {\n      \"public\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVfei1dXQRVeArCMcTDgxJtYg+Fs7\\nV87DjhQbGlRJPyC7SW5TbNNkmvpmi4LeTv6moLVZ7T2nVqiRZbSkD+cf8w==\\n-----END PUBLIC KEY-----\\n\"\n     },\n     \"scheme\": \"ecdsa-sha2-nistp256\",\n     \"x-tuf-on-ci-online-uri\": \"azurekms://npm-tuf-delegate.vault.azure.net/keys/npm-tuf-delegate-2024-08/e2772c1d01ca400da571096889f1660e\"\n    }\n   },\n   \"roles\": [\n    {\n     \"keyids\": [\n      \"5e3a4021b11a425fd0a444f1670457ce5b15bbe036144f2417426f7f4b9721da\"\n     ],\n     \"name\": \"registry.npmjs.org\",\n     \"paths\": [\n      \"registry.npmjs.org/*\"\n     ],\n     \"terminating\": true,\n     \"threshold\": 1\n    }\n   ]\n  },\n  \"expires\": \"2035-06-10T18:17:38Z\",\n  \"spec_version\": \"1.0\",\n  \"targets\": {\n   \"ctfe.pub\": {\n    \"custom\": {\n     \"sigstore\": {\n      \"status\": \"Active\",\n      \"uri\": \"https://ctfe.sigstage.dev/test\",\n      \"usage\": \"CTFE\"\n     }\n    },\n    \"hashes\": {\n     \"sha256\": \"bd7a6812a1f239dfddbbb19d36c7423d21510da56d466ba5018401959cd66037\"\n    },\n    \"length\": 775\n   },\n   \"ctfe_2022.pub\": {\n    \"custom\": {\n     \"sigstore\": {\n      \"status\": \"Active\",\n      \"uri\": \"https://ctfe.sigstage.dev/2022\",\n      \"usage\": \"CTFE\"\n     }\n    },\n    \"hashes\": {\n     \"sha256\": \"910d899c7763563095a0fe684c8477573fedc19a78586de6ecfbfd8f289f5423\"\n    },\n    \"length\": 178\n   },\n   \"ctfe_2022_2.pub\": {\n    \"custom\": {\n     \"sigstore\": {\n      \"status\": \"Active\",\n      \"uri\": \"https://ctfe.sigstage.dev/2022-2\",\n      \"usage\": \"CTFE\"\n     }\n    },\n    \"hashes\": {\n     \"sha256\": \"7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6\"\n    },\n    \"length\": 178\n   },\n   \"fulcio.crt.pem\": {\n    \"custom\": {\n     \"sigstore\": {\n      \"status\": \"Active\",\n      \"uri\": \"https://fulcio.sigstage.dev\",\n      \"usage\": \"Fulcio\"\n     }\n    },\n    \"hashes\": {\n     \"sha256\": \"0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1\"\n    },\n    \"length\": 741\n   },\n   \"fulcio_intermediate.crt.pem\": {\n    \"custom\": {\n     \"sigstore\": {\n      \"status\": \"Active\",\n      \"uri\": \"https://fulcio.sigstage.dev\",\n      \"usage\": \"Fulcio\"\n     }\n    },\n    \"hashes\": {\n     \"sha256\": \"782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b\"\n    },\n    \"length\": 790\n   },\n   \"rekor.pub\": {\n    \"custom\": {\n     \"sigstore\": {\n      \"status\": \"Active\",\n      \"uri\": \"https://rekor.sigstage.dev\",\n      \"usage\": \"Rekor\"\n     }\n    },\n    \"hashes\": {\n     \"sha256\": \"1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959\"\n    },\n    \"length\": 178\n   },\n   \"signing_config.json\": {\n    \"hashes\": {\n     \"sha256\": \"bf52f4aa7dc05849a6c8c760f5ae2ea4047b03b59505d9280efe02a1ec63c6e8\"\n    },\n    \"length\": 220\n   },\n   \"signing_config.v0.2.json\": {\n    \"hashes\": {\n     \"sha256\": \"0f395087486ba318321eda478d847962b1dd89846c7dc6e95752a6b110669393\"\n    },\n    \"length\": 1022\n   },\n   \"trusted_root.json\": {\n    \"hashes\": {\n     \"sha256\": \"ed6a9cf4e7c2e3297a4b5974fce0d17132f03c63512029d7aa3a402b43acab49\"\n    },\n    \"length\": 6824\n   }\n  },\n  \"version\": 17,\n  \"x-tuf-on-ci-expiry-period\": 3650,\n  \"x-tuf-on-ci-signing-period\": 365\n }\n}"
  },
  {
    "path": "test/assets/staging-tuf/targets/0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1.fulcio.crt.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAq\nMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy\nMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUu\nZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9\nBUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUEC\nCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNj\nMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9C\nMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6H\nj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm\n45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTr\ny3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/staging-tuf/targets/0f395087486ba318321eda478d847962b1dd89846c7dc6e95752a6b110669393.signing_config.v0.2.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n  \"caUrls\": [\n    {\n      \"url\": \"https://fulcio.sigstage.dev\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2022-04-14T21:38:40Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"oidcUrls\": [\n    {\n      \"url\": \"https://oauth2.sigstage.dev/auth\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-16T00:00:00Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"rekorTlogUrls\": [\n    {\n      \"url\": \"https://rekor.sigstage.dev\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2021-01-12T11:53:27Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"tsaUrls\": [\n    {\n      \"url\": \"https://timestamp.sigstage.dev/api/v1/timestamp\",\n      \"majorApiVersion\": 1,\n      \"validFor\": {\n        \"start\": \"2025-04-09T00:00:00Z\"\n      },\n      \"operator\": \"sigstore.dev\"\n    }\n  ],\n  \"rekorTlogConfig\": {\n    \"selector\": \"ANY\"\n  },\n  \"tsaConfig\": {\n    \"selector\": \"ANY\"\n  }\n}"
  },
  {
    "path": "test/assets/staging-tuf/targets/1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959.rekor.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9\nnYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "test/assets/staging-tuf/targets/7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6.ctfe_2022_2.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHq\nc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "test/assets/staging-tuf/targets/782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b.fulcio_intermediate.crt.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAq\nMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy\nMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUu\nZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB\nBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9Kt\nNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWI\nJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEF\nBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQF\nGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjO\nPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1O\nHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/\nKX1SBrKQu220FmVL0Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/staging-tuf/targets/ed6a9cf4e7c2e3297a4b5974fce0d17132f03c63512029d7aa3a402b43acab49.trusted_root.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n  \"tlogs\": [\n    {\n      \"baseUrl\": \"https://rekor.sigstage.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2021-01-12T11:53:27Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://log2025-alpha1.rekor.sigstage.dev\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MCowBQYDK2VwAyEAPn+AREHoBaZ7wgS1zBqpxmLSGnyhxXj4lFxSdWVB8o8=\",\n        \"keyDetails\": \"PKIX_ED25519\",\n        \"validFor\": {\n          \"start\": \"2025-04-16T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck=\"\n      }\n    }\n  ],\n  \"certificateAuthorities\": [\n    {\n      \"subject\": {\n        \"organization\": \"sigstore.dev\",\n        \"commonName\": \"sigstore\"\n      },\n      \"uri\": \"https://fulcio.sigstage.dev\",\n      \"certChain\": {\n        \"certificates\": [\n          {\n            \"rawBytes\": \"MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==\"\n          },\n          {\n            \"rawBytes\": \"MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=\"\n          }\n        ]\n      },\n      \"validFor\": {\n        \"start\": \"2022-04-14T21:38:40Z\"\n      }\n    }\n  ],\n  \"ctlogs\": [\n    {\n      \"baseUrl\": \"https://ctfe.sigstage.dev/test\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==\",\n        \"keyDetails\": \"PKCS1_RSA_PKCS1V5\",\n        \"validFor\": {\n          \"start\": \"2021-03-14T00:00:00Z\",\n          \"end\": \"2022-07-31T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://ctfe.sigstage.dev/2022\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2022-07-01T00:00:00Z\",\n          \"end\": \"2022-07-31T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=\"\n      }\n    },\n    {\n      \"baseUrl\": \"https://ctfe.sigstage.dev/2022-2\",\n      \"hashAlgorithm\": \"SHA2_256\",\n      \"publicKey\": {\n        \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==\",\n        \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n        \"validFor\": {\n          \"start\": \"2022-07-01T00:00:00Z\"\n        }\n      },\n      \"logId\": {\n        \"keyId\": \"KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=\"\n      }\n    }\n  ],\n  \"timestampAuthorities\": [\n    {\n      \"subject\": {\n        \"organization\": \"sigstore.dev\",\n        \"commonName\": \"sigstore-tsa-selfsigned\"\n      },\n      \"uri\": \"https://timestamp.sigstage.dev/api/v1/timestamp\",\n      \"certChain\": {\n        \"certificates\": [\n          {\n            \"rawBytes\": \"MIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6\"\n          },\n          {\n            \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUCPExEFKiQh0dP4sp5ltmSYSSkFUwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATt0tIDWyo4ARfL9BaSo0W5bJQEbKJTU/u7llvdjSI5aTkOAJa8tixn2+LEfPG4dMFdsMPtsIuU1qn2OqFiuMk6vHv/c+az25RQVY1oo50iMb0jIL3N4FgwhPFpZnCbQPOjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAKBggqhkjOPQQDAwNpADBmAjEA2MI1VXgbf3dUOSc95hSRypBKOab18eh2xzQtxUsHvWeY+1iFgyMluUuNR6taoSmFAjEA31m2czguZhKYX+4JSKu5pRYhBTXAd8KKQ3xdPRX/qCaLvT2qJAEQ1YQM3EJRrtI7\"\n          }\n        ]\n      },\n      \"validFor\": {\n        \"start\": \"2025-04-09T00:00:00Z\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "test/assets/staging-tuf/timestamp.json",
    "content": "{\n \"signatures\": [\n  {\n   \"keyid\": \"c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4\",\n   \"sig\": \"3046022100fedb5a3d1a3c461c1337d7535edca8012fb0ab8da31315dbdf22b7f38f76973e022100a87967789d2d2942919dcc4f33def8ee74745f577ff0ef5479cc9f573842e8de\"\n  }\n ],\n \"signed\": {\n  \"_type\": \"timestamp\",\n  \"expires\": \"2025-07-29T13:28:44Z\",\n  \"meta\": {\n   \"snapshot.json\": {\n    \"version\": 16\n   }\n  },\n  \"spec_version\": \"1.0\",\n  \"version\": 353\n }\n}"
  },
  {
    "path": "test/assets/trust_config/config.badtype.json",
    "content": "{\n    \"mediaType\": \"bad-media-type\",\n    \"trustedRoot\": {\n        \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n        \"tlogs\": [\n            {\n                \"baseUrl\": \"https://rekor.sigstore.dev\",\n                \"hashAlgorithm\": \"SHA2_256\",\n                \"publicKey\": {\n                    \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==\",\n                    \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                    \"validFor\": {\n                        \"start\": \"2021-01-12T11:53:27.000Z\"\n                    }\n                },\n                \"logId\": {\n                    \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n                }\n            }\n        ],\n        \"certificateAuthorities\": [\n            {\n                \"subject\": {\n                    \"organization\": \"sigstore.dev\",\n                    \"commonName\": \"sigstore\"\n                },\n                \"uri\": \"https://fulcio.sigstore.dev\",\n                \"certChain\": {\n                    \"certificates\": [\n                        {\n                            \"rawBytes\": \"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\"\n                        }\n                    ]\n                },\n                \"validFor\": {\n                    \"start\": \"2021-03-07T03:20:29.000Z\",\n                    \"end\": \"2022-12-31T23:59:59.999Z\"\n                }\n            },\n            {\n                \"subject\": {\n                    \"organization\": \"sigstore.dev\",\n                    \"commonName\": \"sigstore\"\n                },\n                \"uri\": \"https://fulcio.sigstore.dev\",\n                \"certChain\": {\n                    \"certificates\": [\n                        {\n                            \"rawBytes\": \"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=\"\n                        },\n                        {\n                            \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\"\n                        }\n                    ]\n                },\n                \"validFor\": {\n                    \"start\": \"2022-04-13T20:06:15.000Z\"\n                }\n            }\n        ],\n        \"ctlogs\": [\n            {\n                \"baseUrl\": \"https://ctfe.sigstore.dev/test\",\n                \"hashAlgorithm\": \"SHA2_256\",\n                \"publicKey\": {\n                    \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\",\n                    \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                    \"validFor\": {\n                        \"start\": \"2021-03-14T00:00:00.000Z\",\n                        \"end\": \"2022-10-31T23:59:59.999Z\"\n                    }\n                },\n                \"logId\": {\n                    \"keyId\": \"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=\"\n                }\n            },\n            {\n                \"baseUrl\": \"https://ctfe.sigstore.dev/2022\",\n                \"hashAlgorithm\": \"SHA2_256\",\n                \"publicKey\": {\n                    \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==\",\n                    \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                    \"validFor\": {\n                        \"start\": \"2022-10-20T00:00:00.000Z\"\n                    }\n                },\n                \"logId\": {\n                    \"keyId\": \"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=\"\n                }\n            }\n        ],\n        \"timestampAuthorities\": [\n            {\n                \"subject\": {\n                    \"organization\": \"GitHub, Inc.\",\n                    \"commonName\": \"Internal Services Root\"\n                },\n                \"certChain\": {\n                    \"certificates\": [\n                        {\n                            \"rawBytes\": \"MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe\"\n                        },\n                        {\n                            \"rawBytes\": \"MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==\"\n                        },\n                        {\n                            \"rawBytes\": \"MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD\"\n                        }\n                    ]\n                },\n                \"validFor\": {\n                    \"start\": \"2023-04-14T00:00:00.000Z\"\n                }\n            }\n        ]\n    },\n    \"signing_config\": {\n        \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n        \"caUrls\": [\n            {\n                \"url\": \"https://fulcio.example.com\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                \"start\": \"2023-04-14T21:38:40Z\"\n                }\n            },\n            {\n                \"url\": \"https://fulcio-old.example.com\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                \"start\": \"2022-04-14T21:38:40Z\",\n                \"end\": \"2023-04-14T21:38:40Z\"\n                }\n            }\n        ],\n        \"oidcUrls\": [\n            {\n                \"url\": \"https://oauth2.example.com/auth\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2025-04-16T00:00:00Z\"\n                }\n            }\n        ],\n        \"rekorTlogUrls\": [\n            {\n                \"url\": \"https://rekor.example.com\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2021-01-12T11:53:27Z\"\n                }\n            },\n            {\n                \"url\": \"https://rekor-v2.example.com\",\n                \"majorApiVersion\": 2,\n                \"validFor\": {\n                    \"start\": \"2021-01-12T11:53:27Z\"\n                }\n            }\n        ],\n        \"tsaUrls\": [\n            {\n                \"url\": \"https://timestamp.example.com/api/v1/timestamp\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2025-04-09T00:00:00Z\"\n                }\n            }\n        ],\n        \"rekorTlogConfig\": {\n            \"selector\": \"ANY\"\n        },\n        \"tsaConfig\": {\n            \"selector\": \"ANY\"\n        }\n    }\n}\n"
  },
  {
    "path": "test/assets/trust_config/config.v1.json",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.clienttrustconfig.v0.1+json\",\n    \"trustedRoot\": {\n        \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n        \"tlogs\": [\n            {\n                \"baseUrl\": \"https://rekor.sigstore.dev\",\n                \"hashAlgorithm\": \"SHA2_256\",\n                \"publicKey\": {\n                    \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==\",\n                    \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                    \"validFor\": {\n                        \"start\": \"2021-01-12T11:53:27.000Z\"\n                    }\n                },\n                \"logId\": {\n                    \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n                }\n            }\n        ],\n        \"certificateAuthorities\": [\n            {\n                \"subject\": {\n                    \"organization\": \"sigstore.dev\",\n                    \"commonName\": \"sigstore\"\n                },\n                \"uri\": \"https://fulcio.sigstore.dev\",\n                \"certChain\": {\n                    \"certificates\": [\n                        {\n                            \"rawBytes\": \"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\"\n                        }\n                    ]\n                },\n                \"validFor\": {\n                    \"start\": \"2021-03-07T03:20:29.000Z\",\n                    \"end\": \"2022-12-31T23:59:59.999Z\"\n                }\n            },\n            {\n                \"subject\": {\n                    \"organization\": \"sigstore.dev\",\n                    \"commonName\": \"sigstore\"\n                },\n                \"uri\": \"https://fulcio.sigstore.dev\",\n                \"certChain\": {\n                    \"certificates\": [\n                        {\n                            \"rawBytes\": \"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=\"\n                        },\n                        {\n                            \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\"\n                        }\n                    ]\n                },\n                \"validFor\": {\n                    \"start\": \"2022-04-13T20:06:15.000Z\"\n                }\n            }\n        ],\n        \"ctlogs\": [\n            {\n                \"baseUrl\": \"https://ctfe.sigstore.dev/test\",\n                \"hashAlgorithm\": \"SHA2_256\",\n                \"publicKey\": {\n                    \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\",\n                    \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                    \"validFor\": {\n                        \"start\": \"2021-03-14T00:00:00.000Z\",\n                        \"end\": \"2022-10-31T23:59:59.999Z\"\n                    }\n                },\n                \"logId\": {\n                    \"keyId\": \"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=\"\n                }\n            },\n            {\n                \"baseUrl\": \"https://ctfe.sigstore.dev/2022\",\n                \"hashAlgorithm\": \"SHA2_256\",\n                \"publicKey\": {\n                    \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==\",\n                    \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                    \"validFor\": {\n                        \"start\": \"2022-10-20T00:00:00.000Z\"\n                    }\n                },\n                \"logId\": {\n                    \"keyId\": \"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=\"\n                }\n            }\n        ],\n        \"timestampAuthorities\": [\n            {\n                \"subject\": {\n                    \"organization\": \"GitHub, Inc.\",\n                    \"commonName\": \"Internal Services Root\"\n                },\n                \"certChain\": {\n                    \"certificates\": [\n                        {\n                            \"rawBytes\": \"MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe\"\n                        },\n                        {\n                            \"rawBytes\": \"MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==\"\n                        },\n                        {\n                            \"rawBytes\": \"MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD\"\n                        }\n                    ]\n                },\n                \"validFor\": {\n                    \"start\": \"2023-04-14T00:00:00.000Z\"\n                }\n            }\n        ]\n    },\n    \"signing_config\": {\n        \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n        \"caUrls\": [\n            {\n                \"url\": \"https://fulcio.example.com\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2023-04-14T21:38:40Z\"\n                },\n                \"operator\": \"example.com\"\n            },\n            {\n                \"url\": \"https://fulcio-old.example.com\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2022-04-14T21:38:40Z\",\n                    \"end\": \"2023-04-14T21:38:40Z\"\n                },\n                \"operator\": \"example.com\"\n            }\n        ],\n        \"oidcUrls\": [\n            {\n                \"url\": \"https://oauth2.example.com/auth\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2025-04-16T00:00:00Z\"\n                },\n                \"operator\": \"example.com\"\n            }\n        ],\n        \"rekorTlogUrls\": [\n            {\n                \"url\": \"https://rekor.example.com\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2021-01-12T11:53:27Z\"\n                },\n                \"operator\": \"example.com\"\n            },\n            {\n                \"url\": \"https://rekor-v2.example.com\",\n                \"majorApiVersion\": 2,\n                \"validFor\": {\n                    \"start\": \"2021-01-12T11:53:27Z\"\n                },\n                \"operator\": \"example.com\"\n            }\n        ],\n        \"tsaUrls\": [\n            {\n                \"url\": \"https://timestamp.example.com/api/v1/timestamp\",\n                \"majorApiVersion\": 1,\n                \"validFor\": {\n                    \"start\": \"2025-04-09T00:00:00Z\"\n                },\n                \"operator\": \"example.com\"\n            }\n        ],\n        \"rekorTlogConfig\": {\n            \"selector\": \"ANY\"\n        },\n        \"tsaConfig\": {\n            \"selector\": \"ANY\"\n        }\n    }\n}\n"
  },
  {
    "path": "test/assets/trusted_root/certificate_authority.empty.json",
    "content": "{\n  \"subject\": {\n    \"organization\": \"GitHub, Inc.\",\n    \"commonName\": \"Internal Services Root\"\n  },\n  \"certChain\": {\n    \"certificates\": []\n  },\n  \"validFor\": {\n    \"start\": \"2023-04-14T00:00:00.000Z\",\n    \"end\": \"2024-04-14T00:00:00.000Z\"\n  }\n}"
  },
  {
    "path": "test/assets/trusted_root/certificate_authority.json",
    "content": "{\n  \"subject\": {\n    \"organization\": \"GitHub, Inc.\",\n    \"commonName\": \"Internal Services Root\"\n  },\n  \"certChain\": {\n    \"certificates\": [\n      {\n        \"rawBytes\": \"MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe\"\n      },\n      {\n        \"rawBytes\": \"MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==\"\n      },\n      {\n        \"rawBytes\": \"MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD\"\n      }\n    ]\n  },\n  \"validFor\": {\n    \"start\": \"2023-04-14T00:00:00.000Z\",\n    \"end\": \"2024-04-14T00:00:00.000Z\"\n  }\n}"
  },
  {
    "path": "test/assets/trusted_root/certificate_authority.missingroot.json",
    "content": "{\n  \"subject\": {\n    \"organization\": \"GitHub, Inc.\",\n    \"commonName\": \"Internal Services Root\"\n  },\n  \"certChain\": {\n    \"certificates\": [\n      {\n        \"rawBytes\": \"MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe\"\n      },\n      {\n        \"rawBytes\": \"MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==\"\n      }\n    ]\n  },\n  \"validFor\": {\n    \"start\": \"2023-04-14T00:00:00.000Z\",\n    \"end\": \"2024-04-14T00:00:00.000Z\"\n  }\n}"
  },
  {
    "path": "test/assets/trusted_root/trustedroot.badtype.json",
    "content": "{\n    \"mediaType\": \"bad-media-type\",\n    \"tlogs\": [\n        {\n            \"baseUrl\": \"https://rekor.sigstore.dev\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2021-01-12T11:53:27.000Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n            }\n        }\n    ],\n    \"certificateAuthorities\": [\n        {\n            \"subject\": {\n                \"organization\": \"sigstore.dev\",\n                \"commonName\": \"sigstore\"\n            },\n            \"uri\": \"https://fulcio.sigstore.dev\",\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2021-03-07T03:20:29.000Z\",\n                \"end\": \"2022-12-31T23:59:59.999Z\"\n            }\n        },\n        {\n            \"subject\": {\n                \"organization\": \"sigstore.dev\",\n                \"commonName\": \"sigstore\"\n            },\n            \"uri\": \"https://fulcio.sigstore.dev\",\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=\"\n                    },\n                    {\n                        \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2022-04-13T20:06:15.000Z\"\n            }\n        }\n    ],\n    \"ctlogs\": [\n        {\n            \"baseUrl\": \"https://ctfe.sigstore.dev/test\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2021-03-14T00:00:00.000Z\",\n                    \"end\": \"2022-10-31T23:59:59.999Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=\"\n            }\n        },\n        {\n            \"baseUrl\": \"https://ctfe.sigstore.dev/2022\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2022-10-20T00:00:00.000Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=\"\n            }\n        }\n    ],\n    \"timestampAuthorities\": [\n        {\n            \"subject\": {\n                \"organization\": \"GitHub, Inc.\",\n                \"commonName\": \"Internal Services Root\"\n            },\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe\"\n                    },\n                    {\n                        \"rawBytes\": \"MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==\"\n                    },\n                    {\n                        \"rawBytes\": \"MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2023-04-14T00:00:00.000Z\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "test/assets/trusted_root/trustedroot.v1.json",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n    \"tlogs\": [\n        {\n            \"baseUrl\": \"https://rekor.sigstore.dev\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2021-01-12T11:53:27.000Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=\"\n            }\n        }\n    ],\n    \"certificateAuthorities\": [\n        {\n            \"subject\": {\n                \"organization\": \"sigstore.dev\",\n                \"commonName\": \"sigstore\"\n            },\n            \"uri\": \"https://fulcio.sigstore.dev\",\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2021-03-07T03:20:29.000Z\",\n                \"end\": \"2022-12-31T23:59:59.999Z\"\n            }\n        },\n        {\n            \"subject\": {\n                \"organization\": \"sigstore.dev\",\n                \"commonName\": \"sigstore\"\n            },\n            \"uri\": \"https://fulcio.sigstore.dev\",\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=\"\n                    },\n                    {\n                        \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2022-04-13T20:06:15.000Z\"\n            }\n        }\n    ],\n    \"ctlogs\": [\n        {\n            \"baseUrl\": \"https://ctfe.sigstore.dev/test\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2021-03-14T00:00:00.000Z\",\n                    \"end\": \"2022-10-31T23:59:59.999Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=\"\n            }\n        },\n        {\n            \"baseUrl\": \"https://ctfe.sigstore.dev/2022\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2022-10-20T00:00:00.000Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=\"\n            }\n        }\n    ],\n    \"timestampAuthorities\": [\n        {\n            \"subject\": {\n                \"organization\": \"GitHub, Inc.\",\n                \"commonName\": \"Internal Services Root\"\n            },\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe\"\n                    },\n                    {\n                        \"rawBytes\": \"MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==\"\n                    },\n                    {\n                        \"rawBytes\": \"MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2023-04-14T00:00:00.000Z\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "test/assets/trusted_root/trustedroot.v1.local_tlog_ed25519_rekor-tiles.json",
    "content": "{\n    \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n    \"tlogs\": [\n        {\n            \"baseUrl\": \"http://localhost:3003\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MCowBQYDK2VwAyEAREvJyNZGjX6B3DAIuD3BTg9rIwV00GY8Xg5FU+IFDUQ=\",\n                \"keyDetails\": \"PKIX_ED25519\",\n                \"validFor\": {\n                    \"start\": \"1970-01-01T00:00:00Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"tAlACZWkUrif9Z9sOIrpk1ak1I8loRNufk79N6l1SNg=\"\n            }\n        }\n    ],\n    \"certificateAuthorities\": [\n        {\n            \"subject\": {\n                \"organization\": \"sigstore.dev\",\n                \"commonName\": \"sigstore\"\n            },\n            \"uri\": \"https://fulcio.sigstore.dev\",\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2021-03-07T03:20:29.000Z\",\n                \"end\": \"2022-12-31T23:59:59.999Z\"\n            }\n        },\n        {\n            \"subject\": {\n                \"organization\": \"sigstore.dev\",\n                \"commonName\": \"sigstore\"\n            },\n            \"uri\": \"https://fulcio.sigstore.dev\",\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=\"\n                    },\n                    {\n                        \"rawBytes\": \"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2022-04-13T20:06:15.000Z\"\n            }\n        }\n    ],\n    \"ctlogs\": [\n        {\n            \"baseUrl\": \"https://ctfe.sigstore.dev/test\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2021-03-14T00:00:00.000Z\",\n                    \"end\": \"2022-10-31T23:59:59.999Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=\"\n            }\n        },\n        {\n            \"baseUrl\": \"https://ctfe.sigstore.dev/2022\",\n            \"hashAlgorithm\": \"SHA2_256\",\n            \"publicKey\": {\n                \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==\",\n                \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n                \"validFor\": {\n                    \"start\": \"2022-10-20T00:00:00.000Z\"\n                }\n            },\n            \"logId\": {\n                \"keyId\": \"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=\"\n            }\n        }\n    ],\n    \"timestampAuthorities\": [\n        {\n            \"subject\": {\n                \"organization\": \"GitHub, Inc.\",\n                \"commonName\": \"Internal Services Root\"\n            },\n            \"certChain\": {\n                \"certificates\": [\n                    {\n                        \"rawBytes\": \"MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe\"\n                    },\n                    {\n                        \"rawBytes\": \"MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==\"\n                    },\n                    {\n                        \"rawBytes\": \"MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD\"\n                    }\n                ]\n            },\n            \"validFor\": {\n                \"start\": \"2023-04-14T00:00:00.000Z\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "test/assets/tsa/bundle.duplicate.sigstore",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.bundle.v0.3+json\",\n  \"verificationMaterial\": {\n    \"certificate\": {\n      \"rawBytes\": \"MIIC2TCCAl6gAwIBAgIUdmztZIKhChYc16oLF65pX34wgpowCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAyMzM5WhcNMjQxMDMxMTAzMzM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6jFpMi07y77fdwwYmgZ8mMsiORhq9OYO/1KtrJJFHl1yrnN6hpX7vC5affuipObcL3utSgCAnwN1QCAfumx5VqOCAX0wggF5MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUaMSROcZrZvwW7N6tp6yjzkI5QmkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGS4hotJAAABAMARjBEAiB3YxcguZbssCo28dz3BTlBf2RNwL3GOicOIecLahdeJgIgA0RNy/ARrGW2iAnM1PWT/gBgHcQ+wk0hD4FFAmM5JrYwCgYIKoZIzj0EAwMDaQAwZgIxANwxTWEcb9oFkCo63tNd8/ueYAKpsowGyyQs+AX0CE0XJiHjc24HT57G9CP3XYRCnwIxAITQtm0+VvPufhJGvMtn6K0okqWWZFFJQrz0akRlBHHk3osCdhENY0ZBmT8f+59b7Q==\"\n    },\n    \"tlogEntries\": [\n      {\n        \"logIndex\": \"35355462\",\n        \"logId\": {\n          \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n        },\n        \"kindVersion\": {\n          \"kind\": \"hashedrekord\",\n          \"version\": \"0.0.1\"\n        },\n        \"integratedTime\": \"1730370219\",\n        \"inclusionPromise\": {\n          \"signedEntryTimestamp\": \"MEQCIFWlAKfTUTVLdRAkICb7QjK9wWa5clIPSO/I2as7NemMAiAptKOQSwFZsdM/T36yjDhXu4i4i32iy4mLDKFH2SBmAw==\"\n        },\n        \"inclusionProof\": {\n          \"logIndex\": \"3673050\",\n          \"rootHash\": \"CRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\",\n          \"treeSize\": \"3673051\",\n          \"hashes\": [\n            \"PaodjVERCZrJ4m+Ux1vKwci70JNV1o7i6tg+r7emiLU=\",\n            \"hb5Kc++ml8xcjeNY59TfzSSnPGhTQqnl+7VhO4Vr6a8=\",\n            \"pVIutklD+cs4kcBFMp3iPbw/Kn/rWtdwTHwh87zm/so=\",\n            \"eUTldsq4LV/OSczlwUFHxK6yY1+kE/ASoidYXY1zybw=\",\n            \"2rA1/K1G+of0n4dAsYaj4AlV4MWHM7CJz24RmIrEfhs=\",\n            \"P8eXf78ohkRkntQNFfarUtn9Gct7yy+smjM5cersyUg=\",\n            \"3Ul1Loa16XnnGTifeAYy8nlO0JyNIL6E/ZWE1tuIE9w=\",\n            \"mU9v3N0cr/U/8VEM8R56E8z5ScHbeALqtChTUlAmTr4=\",\n            \"70FF4PlelNUMSWeGPKROonP6S+1hpHMe5r5uwLPhuro=\",\n            \"ZS9WKtLvUQYFzFNmaQP+2Gtstl9yM3150pk+oqIMMHU=\",\n            \"lRbgwAuY5l5kOuRQN6uQ8zRQJ5ntgvHUCcNOBOI4Wyg=\"\n          ],\n          \"checkpoint\": {\n            \"envelope\": \"rekor.sigstage.dev - 8202293616175992157\\n3673051\\nCRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\\n\\n— rekor.sigstage.dev 0y8wozBFAiAwPJa5KEL421/AQF8uo81cctm4t9lIY6IGmeH2fV9d1QIhAM6j+/flHM4dEyf5sKCNwyKt9nb9DBLlTHDsPOIrTkyQ\\n\"\n          }\n        },\n        \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGFrYXhGYTd2WkFHV01LMWV1dWMyNkxYY3p0VHJEeUkyT1NmN1lGNXFFNkFpQWkvVTNVbzR6R0RuKytaZTlpUjJEcHMzbElTRXpDTkNmZUJyc0VtMVhHaUE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXlWRU5EUVd3MlowRjNTVUpCWjBsVlpHMTZkRnBKUzJoRGFGbGpNVFp2VEVZMk5YQllNelIzWjNCdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFFVFhoTlZFRjVUWHBOTlZkb1kwNU5hbEY0VFVSTmVFMVVRWHBOZWswMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUyYWtad1RXa3dOM2szTjJaa2QzZFpiV2RhT0cxTmMybFBVbWh4T1U5WlR5OHhTM1FLY2twS1JraHNNWGx5Yms0MmFIQllOM1pETldGbVpuVnBjRTlpWTB3emRYUlRaME5CYm5kT01WRkRRV1oxYlhnMVZuRlBRMEZZTUhkblowWTFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZoVFZOU0NrOWpXbkphZG5kWE4wNDJkSEEyZVdwNmEwazFVVzFyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDB4bldVUldVakJTUVZGSUwwSkRVWGRKYjBWbldWZDRiR1ZIYkhwTWJVNXZXVmQ0YzFsWE5XdGFWVUl3WTIxR2NHSkhPVzFaYld3d1kzazFhZ3BpTWpCM1MxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVXSmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5RM05IQ2tOcGMwZEJVVkZDWnpjNGQwRlJaMFZJVVhkaVlVaFNNR05JVFRaTWVUbG9XVEpPZG1SWE5UQmplVFZ1WWpJNWJtSkhWWFZaTWpsMFRVbEhTa0puYjNJS1FtZEZSVUZrV2pWQloxRkRRa2h6UldWUlFqTkJTRlZCUzNwRE9ETkhhVWw1WlV4b01rTlpjRmh1VVdaVFJHdDRiR2RNZVc1RVVFeFlhMDVCTDNKTGN3cG9ibTlCUVVGSFV6Um9iM1JLUVVGQlFrRk5RVkpxUWtWQmFVSXpXWGhqWjNWYVluTnpRMjh5T0dSNk0wSlViRUptTWxKT2Qwd3pSMDlwWTA5SlpXTk1DbUZvWkdWS1owbG5RVEJTVG5rdlFWSnlSMWN5YVVGdVRURlFWMVF2WjBKblNHTlJLM2RyTUdoRU5FWkdRVzFOTlVweVdYZERaMWxKUzI5YVNYcHFNRVVLUVhkTlJHRlJRWGRhWjBsNFFVNTNlRlJYUldOaU9XOUdhME52TmpOMFRtUTRMM1ZsV1VGTGNITnZkMGQ1ZVZGekswRllNRU5GTUZoS2FVaHFZekkwU0FwVU5UZEhPVU5RTTFoWlVrTnVkMGw0UVVsVVVYUnRNQ3RXZGxCMVptaEtSM1pOZEc0MlN6QnZhM0ZYVjFwR1JrcFJjbm93WVd0U2JFSklTR3N6YjNORENtUm9SVTVaTUZwQ2JWUTRaaXMxT1dJM1VUMDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==\"\n      }\n    ],\n    \"timestampVerificationData\": {\n      \"rfc3161Timestamps\": [\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUZgvCEikoheDobrNm4nFYRaN++jkYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBI8WEfV8xmrlvkaOvelfoYFq1oJACKgeSBC5v4D4Ht4zCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmyRR9E8xnTxNsHflN8+FaAe8AC0s/iArTyOU11g8tnwCIAhCfSMG58DirT9dvDE4qS+lf2u+4c5Zcj8acL/pABxC\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJuBUaVBL+WdW9SX22H1VG/z6MgQYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCADIhONgTZhBhqyN4MtyNBn9si5kmswFNzntVyq29yKSjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlx7HrL52iXvlxB2EbVdH0YBmw9pom2useI+HOoJV3WQCIA2W22DynN8rtB+Rb947RvYmrV8co9tXhU0lRnfoNriU\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUZgvCEikoheDobrNm4nFYRaN++jkYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBI8WEfV8xmrlvkaOvelfoYFq1oJACKgeSBC5v4D4Ht4zCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmyRR9E8xnTxNsHflN8+FaAe8AC0s/iArTyOU11g8tnwCIAhCfSMG58DirT9dvDE4qS+lf2u+4c5Zcj8acL/pABxC\"\n        }\n      ]\n    }\n  },\n  \"messageSignature\": {\n    \"messageDigest\": {\n      \"algorithm\": \"SHA2_256\",\n      \"digest\": \"gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=\"\n    },\n    \"signature\": \"MEQCIHakaxFa7vZAGWMK1euuc26LXcztTrDyI2OSf7YF5qE6AiAi/U3Uo4zGDn++Ze9iR2Dps3lISEzCNCfeBrsEm1XGiA==\"\n  }\n}"
  },
  {
    "path": "test/assets/tsa/bundle.many_timestamp.sigstore",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.bundle.v0.3+json\",\n  \"verificationMaterial\": {\n    \"certificate\": {\n      \"rawBytes\": \"MIIC2TCCAl6gAwIBAgIUdmztZIKhChYc16oLF65pX34wgpowCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAyMzM5WhcNMjQxMDMxMTAzMzM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6jFpMi07y77fdwwYmgZ8mMsiORhq9OYO/1KtrJJFHl1yrnN6hpX7vC5affuipObcL3utSgCAnwN1QCAfumx5VqOCAX0wggF5MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUaMSROcZrZvwW7N6tp6yjzkI5QmkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGS4hotJAAABAMARjBEAiB3YxcguZbssCo28dz3BTlBf2RNwL3GOicOIecLahdeJgIgA0RNy/ARrGW2iAnM1PWT/gBgHcQ+wk0hD4FFAmM5JrYwCgYIKoZIzj0EAwMDaQAwZgIxANwxTWEcb9oFkCo63tNd8/ueYAKpsowGyyQs+AX0CE0XJiHjc24HT57G9CP3XYRCnwIxAITQtm0+VvPufhJGvMtn6K0okqWWZFFJQrz0akRlBHHk3osCdhENY0ZBmT8f+59b7Q==\"\n    },\n    \"tlogEntries\": [\n      {\n        \"logIndex\": \"35355462\",\n        \"logId\": {\n          \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n        },\n        \"kindVersion\": {\n          \"kind\": \"hashedrekord\",\n          \"version\": \"0.0.1\"\n        },\n        \"integratedTime\": \"1730370219\",\n        \"inclusionPromise\": {\n          \"signedEntryTimestamp\": \"MEQCIFWlAKfTUTVLdRAkICb7QjK9wWa5clIPSO/I2as7NemMAiAptKOQSwFZsdM/T36yjDhXu4i4i32iy4mLDKFH2SBmAw==\"\n        },\n        \"inclusionProof\": {\n          \"logIndex\": \"3673050\",\n          \"rootHash\": \"CRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\",\n          \"treeSize\": \"3673051\",\n          \"hashes\": [\n            \"PaodjVERCZrJ4m+Ux1vKwci70JNV1o7i6tg+r7emiLU=\",\n            \"hb5Kc++ml8xcjeNY59TfzSSnPGhTQqnl+7VhO4Vr6a8=\",\n            \"pVIutklD+cs4kcBFMp3iPbw/Kn/rWtdwTHwh87zm/so=\",\n            \"eUTldsq4LV/OSczlwUFHxK6yY1+kE/ASoidYXY1zybw=\",\n            \"2rA1/K1G+of0n4dAsYaj4AlV4MWHM7CJz24RmIrEfhs=\",\n            \"P8eXf78ohkRkntQNFfarUtn9Gct7yy+smjM5cersyUg=\",\n            \"3Ul1Loa16XnnGTifeAYy8nlO0JyNIL6E/ZWE1tuIE9w=\",\n            \"mU9v3N0cr/U/8VEM8R56E8z5ScHbeALqtChTUlAmTr4=\",\n            \"70FF4PlelNUMSWeGPKROonP6S+1hpHMe5r5uwLPhuro=\",\n            \"ZS9WKtLvUQYFzFNmaQP+2Gtstl9yM3150pk+oqIMMHU=\",\n            \"lRbgwAuY5l5kOuRQN6uQ8zRQJ5ntgvHUCcNOBOI4Wyg=\"\n          ],\n          \"checkpoint\": {\n            \"envelope\": \"rekor.sigstage.dev - 8202293616175992157\\n3673051\\nCRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\\n\\n— rekor.sigstage.dev 0y8wozBFAiAwPJa5KEL421/AQF8uo81cctm4t9lIY6IGmeH2fV9d1QIhAM6j+/flHM4dEyf5sKCNwyKt9nb9DBLlTHDsPOIrTkyQ\\n\"\n          }\n        },\n        \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGFrYXhGYTd2WkFHV01LMWV1dWMyNkxYY3p0VHJEeUkyT1NmN1lGNXFFNkFpQWkvVTNVbzR6R0RuKytaZTlpUjJEcHMzbElTRXpDTkNmZUJyc0VtMVhHaUE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXlWRU5EUVd3MlowRjNTVUpCWjBsVlpHMTZkRnBKUzJoRGFGbGpNVFp2VEVZMk5YQllNelIzWjNCdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFFVFhoTlZFRjVUWHBOTlZkb1kwNU5hbEY0VFVSTmVFMVVRWHBOZWswMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUyYWtad1RXa3dOM2szTjJaa2QzZFpiV2RhT0cxTmMybFBVbWh4T1U5WlR5OHhTM1FLY2twS1JraHNNWGx5Yms0MmFIQllOM1pETldGbVpuVnBjRTlpWTB3emRYUlRaME5CYm5kT01WRkRRV1oxYlhnMVZuRlBRMEZZTUhkblowWTFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZoVFZOU0NrOWpXbkphZG5kWE4wNDJkSEEyZVdwNmEwazFVVzFyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDB4bldVUldVakJTUVZGSUwwSkRVWGRKYjBWbldWZDRiR1ZIYkhwTWJVNXZXVmQ0YzFsWE5XdGFWVUl3WTIxR2NHSkhPVzFaYld3d1kzazFhZ3BpTWpCM1MxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVXSmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5RM05IQ2tOcGMwZEJVVkZDWnpjNGQwRlJaMFZJVVhkaVlVaFNNR05JVFRaTWVUbG9XVEpPZG1SWE5UQmplVFZ1WWpJNWJtSkhWWFZaTWpsMFRVbEhTa0puYjNJS1FtZEZSVUZrV2pWQloxRkRRa2h6UldWUlFqTkJTRlZCUzNwRE9ETkhhVWw1WlV4b01rTlpjRmh1VVdaVFJHdDRiR2RNZVc1RVVFeFlhMDVCTDNKTGN3cG9ibTlCUVVGSFV6Um9iM1JLUVVGQlFrRk5RVkpxUWtWQmFVSXpXWGhqWjNWYVluTnpRMjh5T0dSNk0wSlViRUptTWxKT2Qwd3pSMDlwWTA5SlpXTk1DbUZvWkdWS1owbG5RVEJTVG5rdlFWSnlSMWN5YVVGdVRURlFWMVF2WjBKblNHTlJLM2RyTUdoRU5FWkdRVzFOTlVweVdYZERaMWxKUzI5YVNYcHFNRVVLUVhkTlJHRlJRWGRhWjBsNFFVNTNlRlJYUldOaU9XOUdhME52TmpOMFRtUTRMM1ZsV1VGTGNITnZkMGQ1ZVZGekswRllNRU5GTUZoS2FVaHFZekkwU0FwVU5UZEhPVU5RTTFoWlVrTnVkMGw0UVVsVVVYUnRNQ3RXZGxCMVptaEtSM1pOZEc0MlN6QnZhM0ZYVjFwR1JrcFJjbm93WVd0U2JFSklTR3N6YjNORENtUm9SVTVaTUZwQ2JWUTRaaXMxT1dJM1VUMDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==\"\n      }\n    ],\n    \"timestampVerificationData\": {\n      \"rfc3161Timestamps\": [\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUZgvCEikoheDobrNm4nFYRaN++jkYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBI8WEfV8xmrlvkaOvelfoYFq1oJACKgeSBC5v4D4Ht4zCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmyRR9E8xnTxNsHflN8+FaAe8AC0s/iArTyOU11g8tnwCIAhCfSMG58DirT9dvDE4qS+lf2u+4c5Zcj8acL/pABxC\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJuBUaVBL+WdW9SX22H1VG/z6MgQYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCADIhONgTZhBhqyN4MtyNBn9si5kmswFNzntVyq29yKSjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlx7HrL52iXvlxB2EbVdH0YBmw9pom2useI+HOoJV3WQCIA2W22DynN8rtB+Rb947RvYmrV8co9tXhU0lRnfoNriU\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAInPJsUcb45j2wREk+YYo4TWAvEKGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgltd3hQPYZ4mepYN7yuTWP5rls2yho0g5Y3PJxye8ljswgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhAK6CcXH6ZS05vw0kPGz1d8XZ6E4mWBa/uCH6rTlgEG7QAiEA1GVR+SPQ4yaY4KpsuOOHzftnHFaFh1M+nFJ3UQqasEg=\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUIT28EHNSU/e7tTi0z06mlsLKhWcYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHeMIIB2gIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDJw9ILGbCv13QFU6uDNfRYJB7gQqaEJrvGc+KmLjX5MjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIESDBGAiEA8/+MZO19ZWXJQxSS8BuNRPv9kBtCEwKSppWPLwVGv3ICIQCxyttCk9ZiF4H9yqEDS1nM1kaZJ2GwVYNSlAB0UFlGyA==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAKOZ8Vbs/XHOiYrwAu7rwCzfUynwGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQg8EBkMvj98bWZRrUFrIuu1ESqfgZz3bl30t70EcmMmGowgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgGB67fQDA0aQeCox67ZML9eysx+Hh5P7xMc6JqffOUc4CIQCoHzV3vC72XZ8uLdW/bJZmnDsydoMbAGZ6L+9+K2yeLQ==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJK4QC2mcxSdWnCqd1bF0JQo/lgsYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDFl7LJGj2Ly6mrN3rzvyj+h0hRUK4/mvHEgTXCpy9K3DCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlNj866pok1LTFRuzxIfu+h/KJ/kHmKnUfNF4PL2cdgsCIEKRudmJVaifKu72aNwiMB+P1YicRzgl/QGQPNAYDxUe\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUf3SIz6Ht3dk2YgyjaHAaBZs/lKsYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCwUeUZs9/xTDTrX3wn8y4jIrJcg4x1gB0H6eduxRHBeDCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEA5KdiLgcy7vmULd6k+fPCsHKeUv3K/Zy9+S6Sy2tE/jQCICB/GiGCKVP8hXI12VMfoVZMndJAh6t9Zq63JqIqkygW\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAJHdw6fXDetoIFJW9Hhd9B3XjzwnGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgJD6UCRd9vvJ16BbydH4H6VG+7aQvNpRAfk47sO/AXBMwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgGb8MyozlUpYJuYHOFMM3GVMuz3ljepDKTa2c4KvLYQgCIQCWzPS4iv1RAgr45jW+aPt4pmLNvnr2R9rvRyLLeEqvEg==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAMLU2ENJMM+YtgW6Ej17g4dY1BXzGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3DCCAdgCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgo6YdXzqPFS778KAUN8h8L8iBJ2PyMUVNVOXyt3qISu0wgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEYwRAIgMCSlSF5sZxIBJh2KJ3dMdsQNKsuFrBYwxE8BM/ByjM0CICrcfQgicy7Vb0cuaZWKWyB/+1uRJc274srHesI50wWT\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVALZi3hcEqREKdAllVyl2vVL2fYX2GA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3DCCAdgCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgJBXiPjyQL6u9Cfc93/ehFPceaShX4VjKbDHcMtLTa1QwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEYwRAIgUL6zosIZSbdCs/ABNWcHlTuv2S1gN4djN6cXG7BvHv0CIDvwYlb8wMjAZanyWRfotKtYTL7Ye05xxWw9e4fMe38e\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyTADAgEAMIIEwAYJKoZIhvcNAQcCoIIEsTCCBK0CAQMxDTALBglghkgBZQMEAgEwgeMGCyqGSIb3DQEJEAEEoIHTBIHQMIHNAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwITZ8VwQISPxP7v+HsNtCiRc9y60hgPMjAyNDEwMzExMzMxNDRaMAMCAQECCQDeEKL9dsIkd6A0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZ6CCAdAwggHMMIIBcqADAgECAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlMB4XDTI0MTAzMTEwMTY0MloXDTMzMTAzMTEwMTk0MlowMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFsBczJAcgy5m1djVteUhYIoM8q2zWPUkcH2fSPy15JgqzqO71n6/SOhNxqIPQXRH5gSHrFYH37vWOSDnuqwmoKjajBoMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUevzhlsrgOtQ8LOwRBsGN1Pk6dEEwHwYDVR0jBBgwFoAUKQ3ogB8l0b4dI6fWlLD0WdO47VEwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwCgYIKoZIzj0EAwIDSAAwRQIgCFrR3oKDH2v9LWK1zDMIIumPo512ntcfJDpz2KyRPgYCIQDseuNiwsTheN2xQDo6Cyg7uHkjORuxURhQCcoQVyGgpjGCAd0wggHZAgEBMEgwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCwYJYIZIAWUDBAIBoIIBJjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI0MTAzMTEzMzE0NFowLwYJKoZIhvcNAQkEMSIEIPaye/IOiatxE/qZVu+5IGfOsrb/pw5s6EXgMxbHUkGjMIG4BgsqhkiG9w0BCRACLzGBqDCBpTCBojCBnzANBglghkgBZQMEAgMFAARA+uaTVxbYgF5mdEBZiW3fmtlEv57jdNRDoLZyoY4QZhkZLnB7djPjqkzt2KN4BmSfUlVLSs5/pbVOwVGvB0Ck/TBMMDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAgRHMEUCIAuefPhxtIeTzcfmcD2/sct018RGQkUZf6cd/gZbhOUTAiEAyak2wS0GccGyLKmOzLf1qhFPlg0xNvWFsYPXaaU2L50=\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAJLoYqDHwJYORZm20RVvq1WgprF1GA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3DCCAdgCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgJnZkV/5vs89xvZkEe5mY3EPlzMffcAAEzWay+Y5NXfswgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEYwRAIgdfSJnnbg7HItEVtXGJXRKv+mX42wf1+kTWfsK5+RwVUCIFmk0MpQQEvtppMyNpi2aREMgBlyBLZNagy2oEBZsmAJ\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAIg36qKQS62FnKKyVXngSdPZyjLVGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgdIRlDQniLDG+PQC0nFDn+yM7DWnj0HBpgl8vLl5btNQwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhALJZdk70DlAv/ponk9yDbUX8ua5gwFkcL2/F9ITMsL78AiEA03RK1OQutx0a3gEOUT4F7OR8/+/jURQB6AEP1UN9ne4=\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUYnPiMXSrHdM9jSdGvG8SFy1PFBMYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDIxXh6UbkxROrCcmXMMtV5YT4xoVPONPKgdsqr7i6yvjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAmrEaUHQaDJyJ3SgOfT6zEaNoDzWJmXPEZGrTPcndvawCIB+fblhJghtC/A/3z3vBth6eNMRAyUPmgMIrIZNockc/\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAOhMcTpiZiwek9RMeTKHogZlXH5aGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgiW9TS7B6Pj2i8/kOsyc5J6Nlv9B2cs0TmJrKXGiezRAwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgI6iJSt0G769QYyAanhmt3vhdbPCH9XlduqugeMVftZ8CIQDZhCHQ1IlkWwqjqVbdsiyT/g/H0osY8njsNhsU0avyTA==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAKTrtFND23NJeB35euXQDT3Mv+cnGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgoARyH9dgZX1lK113tbvVKZ2ItQWbKqOPQdCBzktaxMQwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhAKZPGGj2Jtjmj4ajvcWuZu/DJqqZ4NDdyTL7Sw5H//XcAiEAnnNpf5gV7he/SK66ptbSM1nI2XkuZvmX+Hc1IS/Sbd8=\"\n        },\n        {\n          \"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\"\n        },\n        {\n          \"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==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUIyUZu6SUFnvc9pDQMVrMd0lYtywYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCHUlruf/vA80MakZUrOnhdzFYjSrM6ip1NfOUCYJ3jLTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAs4ykr1TcxjtFAGUX8w/+i2Mb2QHBKOoCm7Mus5ETzBcCIG0FxCjjbOkXw+EQtEaGl1FxISmI2h7HCCQ5G6l/ydY9\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUbTGN52J6iZc7ceiPM8aRd9aiZZAYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCAbSxLrzt+SZjAc5ZcchbyrfWfLq5G3H/xmdA0WD/PwTjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiBfP/ENsJrxKQXYFma4b3PqFpnroHA0fh3dAOi+6ud7agIhAMSjqykzqraZ1v3fZkW7ehF4ybZsnulwbCoho1c0DGHB\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUI0Uj5oFX3HIBcrNERp3Lg3e6HcoYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDe32jIZ6h1TaxrXNUHcjUy8xDINcNt8ZKPw/PbEvhDTTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAigUZ03YjBk3IgQAR7lW3fYvBedhZwCjo1hgbtvEFlKQCICBqO3wm8IU6Iku1I+1+cZphdP8BJzSfFsW5eeDUySoW\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEzDADAgEAMIIEwwYJKoZIhvcNAQcCoIIEtDCCBLACAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAMYTttbsf5TVXLWLsskIChkdBukvGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3jCCAdoCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQg6FCz+9D0Mmk2b0liV/c7PMoEZ1XJOMAGL5RKYyNdaiowgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEgwRgIhALS5vpv8wh0u+lrPU0iTIeKkZ9hvYUmsmsTy+vhN7jt2AiEAmZiJUy5iRu8kA5NtNlbyxVD4G4nQPTVG8+KpJSypg1g=\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyDADAgEAMIIEvwYJKoZIhvcNAQcCoIIEsDCCBKwCAQMxDTALBglghkgBZQMEAgEwgeMGCyqGSIb3DQEJEAEEoIHTBIHQMIHNAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwITLeOwPutMmIy9ptwVRrrV9iHizhgPMjAyNDEwMzExMzMxNDRaMAMCAQECCQDeEKL9dsIkd6A0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZ6CCAdAwggHMMIIBcqADAgECAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlMB4XDTI0MTAzMTEwMTY0MloXDTMzMTAzMTEwMTk0MlowMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIFRpbWVzdGFtcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFsBczJAcgy5m1djVteUhYIoM8q2zWPUkcH2fSPy15JgqzqO71n6/SOhNxqIPQXRH5gSHrFYH37vWOSDnuqwmoKjajBoMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUevzhlsrgOtQ8LOwRBsGN1Pk6dEEwHwYDVR0jBBgwFoAUKQ3ogB8l0b4dI6fWlLD0WdO47VEwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwCgYIKoZIzj0EAwIDSAAwRQIgCFrR3oKDH2v9LWK1zDMIIumPo512ntcfJDpz2KyRPgYCIQDseuNiwsTheN2xQDo6Cyg7uHkjORuxURhQCcoQVyGgpjGCAdwwggHYAgEBMEgwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCwYJYIZIAWUDBAIBoIIBJjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI0MTAzMTEzMzE0NFowLwYJKoZIhvcNAQkEMSIEIL4Zc9dFtdmbazGRScylYeV9frTjeTjZ63HSKXwJLayEMIG4BgsqhkiG9w0BCRACLzGBqDCBpTCBojCBnzANBglghkgBZQMEAgMFAARA+uaTVxbYgF5mdEBZiW3fmtlEv57jdNRDoLZyoY4QZhkZLnB7djPjqkzt2KN4BmSfUlVLSs5/pbVOwVGvB0Ck/TBMMDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAgRGMEQCIF54Y6QhPoj0n7E5ZCU1WnZ1c3TYOwnX2WQRD6R7WD+SAiBy+iSKWo444zGVv2AxLaHG0lp5r1CvTAMqPCE4f7uDuQ==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUTIPW51Kzlup9NpJuarjVlQkXpugYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHeMIIB2gIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDs4QjBqIMyPO2fo/b8SjlNVDPNvaV5li54i43cA2nvRjCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIESDBGAiEAtyu5AUpVmpPwaIEZe0mi5o3UjSgRMcRt6W1tbL7EMT4CIQDT4ddVhhtjKhf5opJ6dD/UXQ14xlrddDBZ9+0jsaQLcg==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUeNmWsBH4ky6lc4VUxeGAzPs6V+QYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCA1NtyjF3jlgAqkHfJDfyb1+D/UOaABhJfm0RhcvpZIvTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAp2Xys7Tfl/WJB6ZFcMxMn3VhLc6MvNTORBHVi3CJhMsCIFfJ3PBILVeko5Qj1tybiqYL8aNKXMIcm5dv0sFVbRLv\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUEFKQvJTgA1QyuhDzBJMqlB7FIeQYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCB4pQo8/QYvXbUleCV6iFElReti9ExJVWTxUAih36suEzCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEArhAhQ1FYvofLmUmAzk8v6ErYEsCE2vngpGoNkmkybZUCIChWYnbfEwZDGTCebuwenfW0ndqFjJbnpcDXArp590NH\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAKmQSfy4m28Bl+diEK6dclIivqAPGA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgwIZoyHcKEnjlV5aHtlhKd2uJ7heBcbA4YuSpQZZ5RkYwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIhAL2Nm9AiCE/6bE3X4zxrJSP5hHXFe/f4p6eenI9tNEgOAiB/cxrD69xFEW6N33wvgDCRE8Q0XC92jKxWbBamgPyjtA==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUWRjdmZLHICtEm70NbMoe3Hm316QYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBK1V9osV6v0ngq5pt5oslEqPQh6NnCTimd5yTtUeVFTTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiAHwVYpCB+br0HeRKTtKJZAdzTaRj6XR72i287BTl3/IwIhAMrra0CD+s+X26mK+BDxBozV4lgpW4ZXLzbL/rC5Mbmx\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUaGDqxc9Rx+1uPcarTyB3gCaSABAYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCOj1ozKtwZFgeqJ48LSRwvQLM27AkujSeXof8BzRuWeTCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiAoIOACPZBemfDo9JCi8f3AD9fLJjvoVNvPL9N1OJFnmAIhAKCUqP+i3pJRrizarje/wUS0/iaOXYh3lLUjETrgVSCB\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUUbP7pOissqOStCSLp3EFafJEtYMYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHeMIIB2gIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCBaWur+DlUeVjUCEpwpXJe3Iq9ba+oMdn6qptW4N2nz7TCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIESDBGAiEAi+cZ8bda1ZHjdhc7ikjwFLaHXw2vDwAQSHGOFpSttzMCIQCBwZgA1XRLEq296IAH4TyNZFRxoxnGGnqWEY7oTfvuCA==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUQsy8bqxaU7nFhuDvs0zk3Kr2680YDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCAjYjcQaMh/tc3YM2g6W0Z5bQAO/uBkHOXR5WbyI/gEOzCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiA3j2dbOrHG6OZEIEOAxe3Usb+7eLfrvnSSFgE+Oa6gwAIhAP9PAQlUqrEfOL7aDbiD+hglBRDJ9RPExRW55+Pn3JSN\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUCemJTTDrv0Wsd9IaZDxMJNYwTwYYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCCTT+cKL9bCmR0gv/53jlas7I+VVxCVCp/HwI6BhhbK5TCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiAaKKMCyJkhh5paSin5JQDWlo+5N/Ca5IIpJcIDV0t9yQIhALnC230cQdM5Zgb02iwGyWYrLw4ib/6qMoFzz5gipRPz\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyzADAgEAMIIEwgYJKoZIhvcNAQcCoIIEszCCBK8CAQMxDTALBglghkgBZQMEAgEwgeUGCyqGSIb3DQEJEAEEoIHVBIHSMIHPAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIVAK18cM7JkaNz2uMeXml3idd62ff6GA8yMDI0MTAzMTEzMzE0NFowAwIBAQIJAN4Qov12wiR3oDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5noIIB0DCCAcwwggFyoAMCAQICFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAxNjQyWhcNMzMxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWwFzMkByDLmbV2NW15SFgigzyrbNY9SRwfZ9I/LXkmCrOo7vWfr9I6E3Gog9BdEfmBIesVgffu9Y5IOe6rCagqNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBR6/OGWyuA61Dws7BEGwY3U+Tp0QTAfBgNVHSMEGDAWgBQpDeiAHyXRvh0jp9aUsPRZ07jtUTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAgNIADBFAiAIWtHegoMfa/0tYrXMMwgi6Y+jnXae1x8kOnPYrJE+BgIhAOx642LCxOF43bFAOjoLKDu4eSM5G7FRGFAJyhBXIaCmMYIB3TCCAdkCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTALBglghkgBZQMEAgGgggEmMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMDMxMTMzMTQ0WjAvBgkqhkiG9w0BCQQxIgQgEM/0lJtEhZJ87BF6E2NW7g58LBMYJhEkHkzX+pSbTykwgbgGCyqGSIb3DQEJEAIvMYGoMIGlMIGiMIGfMA0GCWCGSAFlAwQCAwUABED65pNXFtiAXmZ0QFmJbd+a2US/nuN01EOgtnKhjhBmGRkucHt2M+OqTO3Yo3gGZJ9SVUtKzn+ltU7BUa8HQKT9MEwwNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAoGCCqGSM49BAMCBEcwRQIgAi0YPVA7nVjTmw4punVccQlIuxOSQrL+03CpHSPY3lMCIQCaXUl5aPoFM2263M1Fi37a87KCvc0UyZTWDGV6gbnPcw==\"\n        }\n      ]\n    }\n  },\n  \"messageSignature\": {\n    \"messageDigest\": {\n      \"algorithm\": \"SHA2_256\",\n      \"digest\": \"gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=\"\n    },\n    \"signature\": \"MEQCIHakaxFa7vZAGWMK1euuc26LXcztTrDyI2OSf7YF5qE6AiAi/U3Uo4zGDn++Ze9iR2Dps3lISEzCNCfeBrsEm1XGiA==\"\n  }\n}"
  },
  {
    "path": "test/assets/tsa/bundle.txt",
    "content": "DO NOT MODIFY ME!\n\nthis is \"bundle.txt\", a sample input for sigstore-python's unit tests.\n\nDO NOT MODIFY ME!\n"
  },
  {
    "path": "test/assets/tsa/bundle.txt.late_timestamp.sigstore",
    "content": "{\"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==\"}}\n"
  },
  {
    "path": "test/assets/tsa/bundle.txt.sigstore",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.bundle.v0.3+json\",\n  \"verificationMaterial\": {\n    \"certificate\": {\n      \"rawBytes\": \"MIIC2TCCAl6gAwIBAgIUdmztZIKhChYc16oLF65pX34wgpowCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMDMxMTAyMzM5WhcNMjQxMDMxMTAzMzM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6jFpMi07y77fdwwYmgZ8mMsiORhq9OYO/1KtrJJFHl1yrnN6hpX7vC5affuipObcL3utSgCAnwN1QCAfumx5VqOCAX0wggF5MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUaMSROcZrZvwW7N6tp6yjzkI5QmkwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwLgYDVR0RAQH/BCQwIoEgYWxleGlzLmNoYWxsYW5kZUB0cmFpbG9mYml0cy5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGS4hotJAAABAMARjBEAiB3YxcguZbssCo28dz3BTlBf2RNwL3GOicOIecLahdeJgIgA0RNy/ARrGW2iAnM1PWT/gBgHcQ+wk0hD4FFAmM5JrYwCgYIKoZIzj0EAwMDaQAwZgIxANwxTWEcb9oFkCo63tNd8/ueYAKpsowGyyQs+AX0CE0XJiHjc24HT57G9CP3XYRCnwIxAITQtm0+VvPufhJGvMtn6K0okqWWZFFJQrz0akRlBHHk3osCdhENY0ZBmT8f+59b7Q==\"\n    },\n    \"tlogEntries\": [\n      {\n        \"logIndex\": \"35355462\",\n        \"logId\": {\n          \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n        },\n        \"kindVersion\": {\n          \"kind\": \"hashedrekord\",\n          \"version\": \"0.0.1\"\n        },\n        \"integratedTime\": \"1730370219\",\n        \"inclusionPromise\": {\n          \"signedEntryTimestamp\": \"MEQCIFWlAKfTUTVLdRAkICb7QjK9wWa5clIPSO/I2as7NemMAiAptKOQSwFZsdM/T36yjDhXu4i4i32iy4mLDKFH2SBmAw==\"\n        },\n        \"inclusionProof\": {\n          \"logIndex\": \"3673050\",\n          \"rootHash\": \"CRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\",\n          \"treeSize\": \"3673051\",\n          \"hashes\": [\n            \"PaodjVERCZrJ4m+Ux1vKwci70JNV1o7i6tg+r7emiLU=\",\n            \"hb5Kc++ml8xcjeNY59TfzSSnPGhTQqnl+7VhO4Vr6a8=\",\n            \"pVIutklD+cs4kcBFMp3iPbw/Kn/rWtdwTHwh87zm/so=\",\n            \"eUTldsq4LV/OSczlwUFHxK6yY1+kE/ASoidYXY1zybw=\",\n            \"2rA1/K1G+of0n4dAsYaj4AlV4MWHM7CJz24RmIrEfhs=\",\n            \"P8eXf78ohkRkntQNFfarUtn9Gct7yy+smjM5cersyUg=\",\n            \"3Ul1Loa16XnnGTifeAYy8nlO0JyNIL6E/ZWE1tuIE9w=\",\n            \"mU9v3N0cr/U/8VEM8R56E8z5ScHbeALqtChTUlAmTr4=\",\n            \"70FF4PlelNUMSWeGPKROonP6S+1hpHMe5r5uwLPhuro=\",\n            \"ZS9WKtLvUQYFzFNmaQP+2Gtstl9yM3150pk+oqIMMHU=\",\n            \"lRbgwAuY5l5kOuRQN6uQ8zRQJ5ntgvHUCcNOBOI4Wyg=\"\n          ],\n          \"checkpoint\": {\n            \"envelope\": \"rekor.sigstage.dev - 8202293616175992157\\n3673051\\nCRqsDV1BUlLRUUf4Bs6DhN3QyncQxgUzjcqlr1Un5p4=\\n\\n— rekor.sigstage.dev 0y8wozBFAiAwPJa5KEL421/AQF8uo81cctm4t9lIY6IGmeH2fV9d1QIhAM6j+/flHM4dEyf5sKCNwyKt9nb9DBLlTHDsPOIrTkyQ\\n\"\n          }\n        },\n        \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGFrYXhGYTd2WkFHV01LMWV1dWMyNkxYY3p0VHJEeUkyT1NmN1lGNXFFNkFpQWkvVTNVbzR6R0RuKytaZTlpUjJEcHMzbElTRXpDTkNmZUJyc0VtMVhHaUE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXlWRU5EUVd3MlowRjNTVUpCWjBsVlpHMTZkRnBKUzJoRGFGbGpNVFp2VEVZMk5YQllNelIzWjNCdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJlRTFFVFhoTlZFRjVUWHBOTlZkb1kwNU5hbEY0VFVSTmVFMVVRWHBOZWswMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUyYWtad1RXa3dOM2szTjJaa2QzZFpiV2RhT0cxTmMybFBVbWh4T1U5WlR5OHhTM1FLY2twS1JraHNNWGx5Yms0MmFIQllOM1pETldGbVpuVnBjRTlpWTB3emRYUlRaME5CYm5kT01WRkRRV1oxYlhnMVZuRlBRMEZZTUhkblowWTFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZoVFZOU0NrOWpXbkphZG5kWE4wNDJkSEEyZVdwNmEwazFVVzFyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDB4bldVUldVakJTUVZGSUwwSkRVWGRKYjBWbldWZDRiR1ZIYkhwTWJVNXZXVmQ0YzFsWE5XdGFWVUl3WTIxR2NHSkhPVzFaYld3d1kzazFhZ3BpTWpCM1MxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVXSmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5RM05IQ2tOcGMwZEJVVkZDWnpjNGQwRlJaMFZJVVhkaVlVaFNNR05JVFRaTWVUbG9XVEpPZG1SWE5UQmplVFZ1WWpJNWJtSkhWWFZaTWpsMFRVbEhTa0puYjNJS1FtZEZSVUZrV2pWQloxRkRRa2h6UldWUlFqTkJTRlZCUzNwRE9ETkhhVWw1WlV4b01rTlpjRmh1VVdaVFJHdDRiR2RNZVc1RVVFeFlhMDVCTDNKTGN3cG9ibTlCUVVGSFV6Um9iM1JLUVVGQlFrRk5RVkpxUWtWQmFVSXpXWGhqWjNWYVluTnpRMjh5T0dSNk0wSlViRUptTWxKT2Qwd3pSMDlwWTA5SlpXTk1DbUZvWkdWS1owbG5RVEJTVG5rdlFWSnlSMWN5YVVGdVRURlFWMVF2WjBKblNHTlJLM2RyTUdoRU5FWkdRVzFOTlVweVdYZERaMWxKUzI5YVNYcHFNRVVLUVhkTlJHRlJRWGRhWjBsNFFVNTNlRlJYUldOaU9XOUdhME52TmpOMFRtUTRMM1ZsV1VGTGNITnZkMGQ1ZVZGekswRllNRU5GTUZoS2FVaHFZekkwU0FwVU5UZEhPVU5RTTFoWlVrTnVkMGw0UVVsVVVYUnRNQ3RXZGxCMVptaEtSM1pOZEc0MlN6QnZhM0ZYVjFwR1JrcFJjbm93WVd0U2JFSklTR3N6YjNORENtUm9SVTVaTUZwQ2JWUTRaaXMxT1dJM1VUMDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==\"\n      }\n    ],\n    \"timestampVerificationData\": {\n      \"rfc3161Timestamps\": [\n        {\n          \"signedTimestamp\": \"MIIEgDADAgEAMIIEdwYJKoZIhvcNAQcCoIIEaDCCBGQCAQMxDTALBglghkgBZQMEAgEwgc8GCyqGSIb3DQEJEAEEoIG/BIG8MIG5AgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgF2fxhmJk3kyzmUDGBhn8kEIYUvISuRhDjuVtN7jtKFsCFCfDYd/d4RagLKlLgkIpKC2V2+RxGA8yMDI0MTAzMTEwMjMzOVowAwIBAQIUN08D3ZVEkDYmzP0I9qnAMAsttoWgNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggGoMIIBpAIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCB8zAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI0MTAzMTEwMjMzOVowLwYJKoZIhvcNAQkEMSIEIIwtSBR2KyJoDjyGAgwVokItIcv6NZXcq6WsrdZ7xm2eMIGFBgsqhkiG9w0BCRACLzF2MHQwcjBwBCB6Z+5bJvtyJlSFYWzFldMxC5t4LAmOKRAO8Y3HALjAOTBMMDSkMjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQuTU+Mi1mI5Kgj6Yep2RJ9xEhhLTAKBggqhkjOPQQDAgRGMEQCIAY58TwPQDEm1hewp/Vn7ovaby/hnPzwxzRQulfj7+wvAiAAsjqb+meU6oJVgYia9YWVxeMAC+27c4NgtZsn3lXCJQ==\"\n        },\n        {\n          \"signedTimestamp\": \"MIIEyjADAgEAMIIEwQYJKoZIhvcNAQcCoIIEsjCCBK4CAQMxDTALBglghkgBZQMEAgEwgeQGCyqGSIb3DQEJEAEEoIHUBIHRMIHOAgEBBgkrBgEEAYO/MAIwUTANBglghkgBZQMEAgMFAARAm3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQwIUJK4QC2mcxSdWnCqd1bF0JQo/lgsYDzIwMjQxMDMxMTMzMTQ0WjADAgEBAgkA3hCi/XbCJHegNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHQMIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKYxggHdMIIB2QIBATBIMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBJbnRlcm1lZGlhdGUCFC5NT4yLWYjkqCPph6nZEn3ESGEtMAsGCWCGSAFlAwQCAaCCASYwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMzExMzMxNDRaMC8GCSqGSIb3DQEJBDEiBCDFl7LJGj2Ly6mrN3rzvyj+h0hRUK4/mvHEgTXCpy9K3DCBuAYLKoZIhvcNAQkQAi8xgagwgaUwgaIwgZ8wDQYJYIZIAWUDBAIDBQAEQPrmk1cW2IBeZnRAWYlt35rZRL+e43TUQ6C2cqGOEGYZGS5we3Yz46pM7dijeAZkn1JVS0rOf6W1TsFRrwdApP0wTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIERzBFAiEAlNj866pok1LTFRuzxIfu+h/KJ/kHmKnUfNF4PL2cdgsCIEKRudmJVaifKu72aNwiMB+P1YicRzgl/QGQPNAYDxUe\"\n        }\n      ]\n    }\n  },\n  \"messageSignature\": {\n    \"messageDigest\": {\n      \"algorithm\": \"SHA2_256\",\n      \"digest\": \"gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4=\"\n    },\n    \"signature\": \"MEQCIHakaxFa7vZAGWMK1euuc26LXcztTrDyI2OSf7YF5qE6AiAi/U3Uo4zGDn++Ze9iR2Dps3lISEzCNCfeBrsEm1XGiA==\"\n  }\n}"
  },
  {
    "path": "test/assets/tsa/ca.json",
    "content": "{\n  \"subject\": {\n    \"organization\": \"local\",\n    \"commonName\": \"Test TSA Timestamping\"\n  },\n  \"certChain\": {\n    \"certificates\": [\n      {\n        \"rawBytes\": \"MIIBzDCCAXKgAwIBAgIULk1PjItZiOSoI+mHqdkSfcRIYS0wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMzExMDE2NDJaFw0zMzEwMzExMDE5NDJaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARbAXMyQHIMuZtXY1bXlIWCKDPKts1j1JHB9n0j8teSYKs6ju9Z+v0joTcaiD0F0R+YEh6xWB9+71jkg57qsJqCo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFHr84ZbK4DrUPCzsEQbBjdT5OnRBMB8GA1UdIwQYMBaAFCkN6IAfJdG+HSOn1pSw9FnTuO1RMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0gAMEUCIAha0d6Cgx9r/S1itcwzCCLpj6Oddp7XHyQ6c9iskT4GAiEA7HrjYsLE4XjdsUA6OgsoO7h5IzkbsVEYUAnKEFchoKY=\"\n      },\n      {\n        \"rawBytes\": \"MIIB0zCCAXigAwIBAgIUGnqrcxtrSpsILclUa/+bCnYeZOUwCgYIKoZIzj0EAwIwKDEOMAwGA1UEChMFbG9jYWwxFjAUBgNVBAMTDVRlc3QgVFNBIFJvb3QwHhcNMjQxMDMxMTAxNDQyWhcNMzQxMDMxMTAxOTQyWjAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2apBvcj3qtsHACafJaOd5Zw874AKK2s5XXdd6jrlVF9h3S6JFgUZ/5MVpYWDNKjgrkqbvhU3RroOGXJ4DyPGSaN4MHYwDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCkN6IAfJdG+HSOn1pSw9FnTuO1RMB8GA1UdIwQYMBaAFO0kKGzBCz7EddTsYBcuwp1VgbhxMAoGCCqGSM49BAMCA0kAMEYCIQDQutW0fTsKlGN4CohrIi/5fMIOqXpjxXswhxiBfCUa/AIhAOe4rlnAGQlmYlBW1uDqt0lw3a/2oAGvHRhDKbiIMPqo\"\n      },\n      {\n        \"rawBytes\": \"MIIBkzCCATqgAwIBAgIUfHAOxJRvpMlmRi3vt7yebkXSb9IwCgYIKoZIzj0EAwIwKDEOMAwGA1UEChMFbG9jYWwxFjAUBgNVBAMTDVRlc3QgVFNBIFJvb3QwHhcNMjQxMDMxMTAxNDQyWhcNMzQxMDMxMTAxOTQyWjAoMQ4wDAYDVQQKEwVsb2NhbDEWMBQGA1UEAxMNVGVzdCBUU0EgUm9vdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDQ2pO3oB5x2HKXp1YpgHB7SCVD1pag46/QUGfQHpyYWOdO4q7uqSx19f2StEszzqrZvpRioo1j6Lwnpp6oQ4P+jQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTtJChswQs+xHXU7GAXLsKdVYG4cTAKBggqhkjOPQQDAgNHADBEAiAFbrVtlmebUMEzUL6JijgYrZhkjUR9VvNO+J2rbQ2eeAIgHUf+TJCnYoq3In8hUlH4D92Fc3Xad6lI0mLfYWm5wpk=\"\n      }\n    ]\n  },\n  \"validFor\": {\n    \"start\": \"2024-10-31T10:16:42.000Z\",\n    \"end\": \"2033-10-31T10:19:42.000Z\"\n  }\n}"
  },
  {
    "path": "test/assets/tsa/trust_config.json",
    "content": "{\n  \"mediaType\": \"application/vnd.dev.sigstore.clienttrustconfig.v0.1+json\",\n  \"trustedRoot\": {\n    \"mediaType\": \"application/vnd.dev.sigstore.trustedroot+json;version=0.1\",\n    \"tlogs\": [\n      {\n        \"baseUrl\": \"https://rekor.sigstage.dev\",\n        \"hashAlgorithm\": \"SHA2_256\",\n        \"publicKey\": {\n          \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==\",\n          \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n          \"validFor\": {\n            \"start\": \"2021-01-12T11:53:27.000Z\"\n          }\n        },\n        \"logId\": {\n          \"keyId\": \"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=\"\n        }\n      }\n    ],\n    \"certificateAuthorities\": [\n      {\n        \"subject\": {\n          \"organization\": \"sigstore.dev\",\n          \"commonName\": \"sigstore\"\n        },\n        \"uri\": \"https://fulcio.sigstage.dev\",\n        \"certChain\": {\n          \"certificates\": [\n            {\n              \"rawBytes\": \"MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==\"\n            },\n            {\n              \"rawBytes\": \"MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=\"\n            }\n          ]\n        },\n        \"validFor\": {\n          \"start\": \"2022-04-14T21:38:40.000Z\"\n        }\n      }\n    ],\n    \"timestampAuthorities\": [\n      {\n        \"subject\": {\n          \"organization\": \"local\",\n          \"commonName\": \"Test TSA Timestamping\"\n        },\n        \"certChain\": {\n          \"certificates\": [\n            {\n              \"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\"\n            },\n            {\n              \"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==\"\n            },\n            {\n              \"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=\"\n            }\n          ]\n        },\n        \"validFor\": {\n          \"start\": \"2024-11-07T14:59:40.000Z\",\n          \"end\": \"2033-11-05T14:59:40.000Z\"\n        }\n      }\n    ],\n    \"ctlogs\": [\n      {\n        \"baseUrl\": \"https://ctfe.sigstage.dev/test\",\n        \"hashAlgorithm\": \"SHA2_256\",\n        \"publicKey\": {\n          \"rawBytes\": \"MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==\",\n          \"keyDetails\": \"PKCS1_RSA_PKCS1V5\",\n          \"validFor\": {\n            \"start\": \"2021-03-14T00:00:00.000Z\",\n            \"end\": \"2022-07-31T00:00:00.000Z\"\n          }\n        },\n        \"logId\": {\n          \"keyId\": \"G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=\"\n        }\n      },\n      {\n        \"baseUrl\": \"https://ctfe.sigstage.dev/2022\",\n        \"hashAlgorithm\": \"SHA2_256\",\n        \"publicKey\": {\n          \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==\",\n          \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n          \"validFor\": {\n            \"start\": \"2022-07-01T00:00:00.000Z\",\n            \"end\": \"2022-07-31T00:00:00.000Z\"\n          }\n        },\n        \"logId\": {\n          \"keyId\": \"++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=\"\n        }\n      },\n      {\n        \"baseUrl\": \"https://ctfe.sigstage.dev/2022-2\",\n        \"hashAlgorithm\": \"SHA2_256\",\n        \"publicKey\": {\n          \"rawBytes\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==\",\n          \"keyDetails\": \"PKIX_ECDSA_P256_SHA_256\",\n          \"validFor\": {\n            \"start\": \"2022-07-01T00:00:00.000Z\"\n          }\n        },\n        \"logId\": {\n          \"keyId\": \"KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=\"\n        }\n      }\n    ]\n  },\n  \"signing_config\": {\n    \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n    \"caUrls\": [\n      {\n        \"url\": \"https://fulcio.sigstage.dev\",\n        \"majorApiVersion\": 1,\n        \"validFor\": {\n          \"start\": \"2022-04-14T21:38:40.000Z\"\n        },\n        \"operator\": \"sigstage.dev\"\n      }\n    ],\n    \"rekorTlogUrls\": [\n      {\n        \"url\": \"https://rekor.sigstage.dev\",\n        \"majorApiVersion\": 1,\n        \"validFor\": {\n          \"start\": \"2021-01-12T11:53:27.000Z\"\n        },\n        \"operator\": \"sigstage.dev\"\n      }\n    ],\n    \"tsaUrls\": [\n      {\n        \"url\": \"placeholder\",\n        \"majorApiVersion\": 1,\n        \"validFor\": {\n          \"start\": \"2024-11-07T14:59:40.000Z\"\n        },\n        \"operator\": \"sigstage.dev\"\n      }\n    ],\n    \"rekorTlogConfig\": {\n      \"selector\": \"ANY\"\n    },\n    \"tsaConfig\": {\n      \"selector\": \"ANY\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/assets/x509/bogus-intermediate-with-eku.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDHjCCAgagAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c\nNivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT\nFcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E\n7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4\nhSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp\ngawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g\nKQIDAQABo1YwVDAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTASBgNVHRMB\nAf8ECDAGAQH/AgEBMAsGA1UdDwQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDAzAN\nBgkqhkiG9w0BAQsFAAOCAQEAhLUXPPHJrVnd9G3OQ95uJSWdzTraRwht0wcGgQEX\njpCXZ3j6ZCT8Q2NXuINujZ7UmZ4DUlqcCaf7GM+Ph2ofZ3u2MMMkkpgFwX4lCdPV\n98OfiGvyEnj32HGQThrHmpPBNlxoqlat0YUfeiDtD4a+g29F1fdzxEhbHfi+E5Dq\ndX4JQmCFI6+z7YJa/OW42CUNzsyrINfdHnoVdeYSDXMobon4GVNS7Cc8ktiV/rPr\nrnetAKdeXTjlux5Bi6EASjqDqOY52TLJxltefTkCZZb3DKvAwKAlATpTdCxp2/Ey\nGTbtonT66FROHJer8cc+706dJKyfpcp/CJchlXNN1V8yhg==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-intermediate.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDCTCCAfGgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c\nNivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT\nFcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E\n7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4\nhSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp\ngawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g\nKQIDAQABo0EwPzAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTASBgNVHRMB\nAf8ECDAGAQH/AgEBMAsGA1UdDwQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAIdCQ\nNkReQcTU3aUCKqWdwCeFswg83lFHchgrH6QGSEwNI+y4YnEeSDs4KoU9ptLfVCoG\nWWYaTnePnzjTcOjYs0439/c2zS936EmVJs6EO55LK2YFKGHzhZARnAKVkiwUcHuZ\nbCrV9M3/muZmEwMXszzZfniREMyQZcfqbJjZZURRdmdK+dmHwHNDecXtbPNxQdB3\nBRGtydZd5PH6ATR9zCz+ds3gRth+JFqzEYZH07BeHaCkRL80N7L8hH+E4+y0vVFJ\nsYfWMolbfPxEDRn6XQ/FROV3Kq2kSUIV09FBtE3b4aICB3ih78Xpzmvhh1g8rp7o\noosP3AhvLCLjpruF/A==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-leaf-invalid-eku.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDGDCCAgCgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c\nNivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT\nFcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E\n7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4\nhSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp\ngawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g\nKQIDAQABo1AwTjAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB\nAf8EAjAAMAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG\n9w0BAQsFAAOCAQEAAftcMInllQxRHV3t3jVJN68M6Bm7rOt761yx3p3DOlP4VG1d\n9ZpNQkFLHxDWRFcChuWbEsVMXxGj7hvISKFgyrumlE10Yfn72SGMMRbHQrIGFHd/\nbV6Xlr2IBqZzvCj1ZgoTb3o7U3I7xl2BP1+ScYhSigfCEh6b0U8TwCzaPE8Rfxik\n8dHpW042MzAyiu0NbwH0iOVyj71Fx05/Hb/abgf3k1MV+0pAGC9cEkAyEORNXUda\n1GcJ60hsfwMf7pUf+3f8OcEsznN7gVSWQn79L2nNN/Uh76Tel7JmoXwm5DgIF3Cg\nfLcj2PQo9MTXVuXkf9uJhgvkhmlElHfygYaYKA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-leaf-invalid-ku.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDFzCCAf+gAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c\nNivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT\nFcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E\n7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4\nhSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp\ngawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g\nKQIDAQABo08wTTAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB\nAf8EAjAAMAoGA1UdDwQDAwEAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3\nDQEBCwUAA4IBAQADtd93gD08PW06DnaOYbvpBnEcNYEIS83N0vVLYFkm1UIbr5Ln\nQwORmsBMwOK04IhbnOMBt1Kb5CymFQkyhLHA2Y0KFnG6BYXKnVWgNYWb2yz6CShq\nKHZ6cu1vp/rADApv1IECZVKb5cZk7fCLk735SBuN8ybGdD3z2y6EINovq0c57GBN\nFY/4zPEHkBMlc+Ki/cv2ZwhQ+cAC/sU+vArjb5CWk8S3XfTaAsV30a41jZdmE30W\nyq4l67M4okuIouR6hxQal3TbsdfVVQUcAxmY1iFIXYmvEE48xHN4y4OjtBBTFM8h\n99ePUcR4QrtPrvTmHIAgNupAZI4TUFamrimh\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-leaf-missing-eku.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDAzCCAeugAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c\nNivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT\nFcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E\n7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4\nhSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp\ngawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g\nKQIDAQABozswOTAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB\nAf8EAjAAMAsGA1UdDwQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAAz58QW5XVZbg\nnzXKXhBYcbRRUqbw6edShLna8yzeB8acsuSwYz4sG4h41Q7opNBd3WhEn1dk6loo\nZWM9NpG+t33LUgIjVsEUgt55kMB2DzBH7HHMsS7eGA7Qo/LX6tt3vX4bKG6HmHOI\nGz7cPr8mRkO/EJHcJxTSRQ1uhQGXfjuBO5F2LSOsAUc8bP8VONJFk3lR/ZoON7qv\n+fGTYUp8qYlLQeANJHgywhTxWzcA0ew8j8+qDuTVsVxQUYqsA8m1TSHIPQXCo5gp\nYU7oyEtGt/ly6CDxVTEJVEZndP5roRhm3oYqCJIl9jDvPg7WTyxMtQ9boBzxVPix\nVRqHa9Q1tQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-leaf.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDGDCCAgCgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNNkHwXjz4h6dG2B+g/c\nNivs9ZVh0HNWKEjxHdSaZQ2CXVjnH91rEj7JTsEZLYsICvWjllYLDCfOmV8lfwOT\nFcPAUqysaA7SF1bH/8KqTAfaYrvrVQaMFxOxOfclmMBc2cN2GtcrLLZrnZuwZ40E\n7Zchx5uCZ7njtKV2JL0cQIpjs+rnNVxqy158/Qf/AceC5c/5hI0WI9mw9uL7nzG4\nhSm/CWvd7fvQC6UV/Qt9BosteVIVB/3oYfstnwhr/8apmvq0z69iOf9/wUGPbKWp\ngawF0l+2Z8xMfvB25d49rXWItywteBlkPViE+ew2Ix7UAQZ55EPZzhhI7Pozou/g\nKQIDAQABo1AwTjAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRMB\nAf8EAjAAMAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG\n9w0BAQsFAAOCAQEATrcFg1ZlHp95eTpHV0O5HP+XAbTh8OE55LkT1VYqDBd64THG\njXvEwfDRiexWR5wkLltGJuHxmJq9avlSXBxObwbamSJu1YYiOumv1Bnxnf+wgrNY\ndz6KTeD18xNNASuDRIBoKoj6OF0wJigUMYJThXGCdke9fivMqV3JWtLzPM39cuu4\nyV72EFBEp3/cVD5rIKK7Zfl4KW/ybpOMtvXCIT9GwnI/BgDuGjHimofwtncqzwRT\ncC8w1w9OIloadHOosOxRaT2PGcAWtNhL/clBj9Wwoc3hO5WCpwHGm50tbqGVGkxy\nuYljP7EnJ12llW2UIB2so60SCqOH5cNv8N60Jw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-root-invalid-ku.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDBjCCAe6gAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9\niaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ\nkiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU\ntQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U\nlTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P\nbo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC\nRwIDAQABoz4wPDAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAPBgNVHRMB\nAf8EBTADAQH/MAsGA1UdDwQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAnPQAk1ng\nm/XwyfFe6uvz7PaNz6wryeWWDlEM4ON48Rrf/QhIsG3NU2/ftQUPv5CcOp7R9Xho\nqrC1YsJNypL/BPA4kdheDBp4HLmmX05FyXEG3l2WCGqC1/ZS3Ye4k9WmnsSCWL85\nYImLXhBk9kwpYfPE2PMq5gqHVEKvRZGv+KzdHqt1LJKHUdgE/OtdFya8Af4N2BbB\nmRAPnC2jYIbApEVpM/kG9ANPZQteSGHsJSfWM2uZcbTpeNL55nVE1oXYGcXDbjUl\nAdYbgBiAk1mzmpGz2HMwLWhy5qK4d01dZvQOQZD+FjwqHNG2uHJQkP+qCeVOdRIC\ntdz2zF8ucjf3DA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-root-missing-ku.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC+TCCAeGgAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9\niaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ\nkiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU\ntQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U\nlTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P\nbo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC\nRwIDAQABozEwLzAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAPBgNVHRMB\nAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA038HUNVxomLhJ8zC1HQpR4fiY\npMvxajYXW+h6wi4LS9TxWtxN86etDZWcc7BNYYqEtmn+TYdg3bpXW7uPMM0tpZ6f\nWUZ+yPGKJi6iyOpYHgJMIy7sbSMZHpkPUeMf9Ye8rILrmP8CfjxuT6cq9RpGDqXf\n+rltrXRzmTSecqEyjs9faxf57LE21+4Jpla3WA6fIzidKcMjbFQqqqUMu9OadXZO\nJZqFP18GThZToZs7pXKNlVvMwNNnCnyrn8WbL4j95IokNwzC7lI5opc+FOGUFEZh\nfnAByZ3AqmSFrcnE3+B5eSfupds4mcHnryqSP4/6xvd26aqs/qkmBJ+QkQO9\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-root-noncritical-bc.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDAzCCAeugAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9\niaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ\nkiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU\ntQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U\nlTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P\nbo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC\nRwIDAQABozswOTAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAMBgNVHRME\nBTADAQH/MAsGA1UdDwQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAOZonwl/4au3b\n/WKLy3OL0WEXbwhl6S4i9PxwtTmXSAO6GhPSLIfMrTQlLyay9L40aQ95dZvnfo2Z\nLOoMMpYOfo15YmtuyEAK7syh+UZTT78UNCqlkc0gSNUed6WucWHrAS90+TbFo/1/\nmFGgYjao7GR755MOTFGlwa2eeYV+bEwGd9k1vTycQIOP4gLyBACP8KUMAlTEHnK2\n0ZJ6GIpHVz5LalYCicxe0COKgKXER3l5YsnSLI5hvupeSld2jvNklCf94xMBOn/1\nTsangKU8zZc4GmbPlb+OqxvXNOnMhCQlz3zj762eMsLI599vUA9g1tHAauf05ZN+\njQ9agtlATw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/bogus-root.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDBjCCAe6gAwIBAgICApowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwac2ln\nc3RvcmUtcHl0aG9uLWJvZ3VzLWNlcnQwIBcNMjMwMTAxMDAwMDAwWhgPMzAyMjA1\nMDQwMDAwMDBaMCUxIzAhBgNVBAMMGnNpZ3N0b3JlLXB5dGhvbi1ib2d1cy1jZXJ0\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRp1FrGW//dRQ8KcaOM9\niaD2VUJw4/H513JuxC/7aQ+7WPfBLQ4wLytoJH9E7eSTrAPjh7lilbYVvL2TFLCJ\nkiaGuOgvOpCN8uxC9ie/r+ui+YgexJwlMjwxfqx67WTXZJtC/GVS45ISfq6MkIwU\ntQWvTPXJnHl2epIXj4el6XIQWQL/koBgUlzbNrfdwZ1NpAm0jNDK44DjXDCwEy8U\nlTDMle1dAb4V80GlF5s87cauNp5sfghz05iqZiTSg4461v/EFH7TElAtuaLqa36P\nbo1OEIkeWyej8n60mFbwY2v4Frb9G9vQMY1gV7vLtetfJpgKKIj/iK/jbsrrhjsC\nRwIDAQABoz4wPDAcBgNVHREEFTATghFib2d1cy5leGFtcGxlLmNvbTAPBgNVHRMB\nAf8EBTADAQH/MAsGA1UdDwQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAGxfbsceU\nWyuT59wIXqNjBd1HybiYEHZQw87o907ovdNZALLZ2URTPllIoNUiaxVa3VjG3ttj\niVVDe1JLbS8/JaG+ZqQkHAorByM1tjxoIFiq+yaBYeg997etgFM1OVhbRNq744LE\n2zPEeYTiokbQwDAeUtYRmo+9vK7gn7iNAb/pYOswMMtcGOZSj7ebvJQwkS5qDGMz\nzJ1pdkRpP0/kaLsZouaTsPiiJp3vV0QvVjOJKT765YF0pQCehl17JehDS6jgXhe5\ngqatlDDm7ALG4bCGbqnC4XLYXaEstD4UbrUEvQ5lnO2+jbgDaOoyC6pzGjvC3p7u\nBX8EoFOjwFfx0w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/assets/x509/build-testcases.py",
    "content": "#!/usr/bin/env python\n\n# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# build-testcases.py: generate some bogus X.509 testcases for sigstore's\n# unit tests.\n#\n# These testcases should already be checked-in; you can re-generate them\n# (with entirely new key material) using:\n#\n#  python build-testcases.py\n#\n# ...while running from this directory.\n\nimport datetime\nimport os\nimport sys\nfrom pathlib import Path\n\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.serialization import (\n    Encoding,\n    load_pem_private_key,\n)\nfrom cryptography.x509.oid import NameOID\n\n\ndef _keypair(priv_key_file: Path):\n    priv_key_bytes: bytes\n    with priv_key_file.open(\"rb\") as f:\n        priv_key_bytes = f.read()\n    priv_key = load_pem_private_key(priv_key_bytes, None)\n    return priv_key.public_key(), priv_key\n\n\n_HERE = Path(__file__).resolve().parent\n_ROOT_PUBKEY, _ROOT_PRIVKEY = _keypair(_HERE / \"root-privkey.pem\")\n_NONROOT_PUBKEY, _ = _keypair(_HERE / \"nonroot-privkey.pem\")\n\n_NOT_VALID_BEFORE_DATE = datetime.datetime(2023, 1, 1)\n_A_VERY_LONG_TIME = datetime.timedelta(days=365 * 1000)\n\n\ndef _builder() -> x509.CertificateBuilder:\n    builder = x509.CertificateBuilder()\n    builder = builder.subject_name(\n        x509.Name(\n            [\n                x509.NameAttribute(NameOID.COMMON_NAME, \"sigstore-python-bogus-cert\"),\n            ]\n        )\n    )\n    builder = builder.issuer_name(\n        x509.Name(\n            [\n                x509.NameAttribute(NameOID.COMMON_NAME, \"sigstore-python-bogus-cert\"),\n            ]\n        )\n    )\n    builder = builder.not_valid_before(_NOT_VALID_BEFORE_DATE)\n    builder = builder.not_valid_after(_NOT_VALID_BEFORE_DATE + _A_VERY_LONG_TIME)\n    builder = builder.serial_number(666)\n    builder = builder.add_extension(\n        x509.SubjectAlternativeName([x509.DNSName(\"bogus.example.com\")]), critical=False\n    )\n    return builder\n\n\ndef _finalize(\n    builder: x509.CertificateBuilder, *, pubkey=_ROOT_PUBKEY, privkey=_ROOT_PRIVKEY\n) -> x509.Certificate:\n    builder = builder.public_key(pubkey)\n    return builder.sign(private_key=privkey, algorithm=hashes.SHA256())\n\n\ndef _dump(cert: x509.Certificate, filename: Path):\n    pem = cert.public_bytes(Encoding.PEM)\n    if not filename.exists() or os.getenv(\"TESTCASE_OVERWRITE\"):\n        print(f\"[+] writing: {filename}\", file=sys.stderr)\n        filename.write_bytes(pem)\n    else:\n        print(f\"[+] skipping: {filename}\", file=sys.stderr)\n\n\ndef bogus_root() -> x509.Certificate:\n    \"\"\"\n    A valid root CA certificate.\n    \"\"\"\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=True, path_length=None),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=True,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n\n    return _finalize(builder)\n\n\ndef bogus_root_noncritical_bc() -> x509.Certificate:\n    \"\"\"\n    An invalid root CA certificate, due to the BasicConstraints\n    extension being marked as non-critical.\n    \"\"\"\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=True, path_length=None),\n        critical=False,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=True,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n\n    return _finalize(builder)\n\n\ndef bogus_root_missing_ku() -> x509.Certificate:\n    \"\"\"\n    An invalid root CA certificate, due to a missing\n    KeyUsage extension.\n    \"\"\"\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=True, path_length=None),\n        critical=True,\n    )\n\n    return _finalize(builder)\n\n\ndef bogus_root_invalid_ku() -> x509.Certificate:\n    \"\"\"\n    An invalid root CA certificate, due to inconsistent\n    KeyUsage state (KU.keyCertSign <> BC.ca)\n    \"\"\"\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=True, path_length=None),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=False,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n\n    return _finalize(builder)\n\n\ndef bogus_intermediate() -> x509.Certificate:\n    \"\"\"\n    A valid intermediate CA certificate, for Sigstore purposes.\n    \"\"\"\n\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=True, path_length=1),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=True,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n\n    return _finalize(builder, pubkey=_NONROOT_PUBKEY)\n\n\ndef bogus_intermediate_with_eku() -> x509.Certificate:\n    \"\"\"\n    A valid intermediate CA certificate, for Sigstore purposes.\n\n    This is like `bogus_intermediate`, except that it also contains a\n    code signing EKU entitlement to make sure we don't treat\n    that as an incorrect signal.\n    \"\"\"\n\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=True, path_length=1),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=True,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n    builder = builder.add_extension(\n        x509.ExtendedKeyUsage(usages=[x509.OID_CODE_SIGNING]),\n        critical=False,\n    )\n\n    return _finalize(builder, pubkey=_NONROOT_PUBKEY)\n\n\ndef bogus_leaf() -> x509.Certificate:\n    \"\"\"\n    A valid leaf certificate, for Sigstore purposes.\n    \"\"\"\n\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=False, path_length=None),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=False,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n    builder = builder.add_extension(\n        x509.ExtendedKeyUsage(usages=[x509.OID_CODE_SIGNING]),\n        critical=False,\n    )\n\n    return _finalize(builder, pubkey=_NONROOT_PUBKEY)\n\n\ndef bogus_leaf_invalid_ku() -> x509.Certificate:\n    \"\"\"\n    An invalid leaf certificate (for Sigstore purposes), due to an invalid\n    KeyUsage (lacking the digitalSignature entitlement).\n    \"\"\"\n\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=False, path_length=None),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=False,\n            key_cert_sign=False,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n    builder = builder.add_extension(\n        x509.ExtendedKeyUsage(usages=[x509.OID_CODE_SIGNING]),\n        critical=False,\n    )\n\n    return _finalize(builder, pubkey=_NONROOT_PUBKEY)\n\n\ndef bogus_leaf_invalid_eku() -> x509.Certificate:\n    \"\"\"\n    An invalid leaf certificate (for Sigstore purposes), due to an\n    invalid ExtendedKeyUsage (lacking the code signing entitlement).\n    \"\"\"\n\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=False, path_length=None),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=False,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n    builder = builder.add_extension(\n        x509.ExtendedKeyUsage(usages=[x509.OID_SERVER_AUTH]),\n        critical=False,\n    )\n\n    return _finalize(builder, pubkey=_NONROOT_PUBKEY)\n\n\ndef bogus_leaf_missing_eku() -> x509.Certificate:\n    \"\"\"\n    An invalid leaf certificate (for Sigstore purposes), due to a\n    missing ExtendedKeyUsage extension.\n    \"\"\"\n\n    builder = _builder()\n    builder = builder.add_extension(\n        x509.BasicConstraints(ca=False, path_length=None),\n        critical=True,\n    )\n    builder = builder.add_extension(\n        x509.KeyUsage(\n            digital_signature=True,\n            key_cert_sign=False,\n            content_commitment=False,\n            key_encipherment=False,\n            data_encipherment=False,\n            key_agreement=False,\n            crl_sign=False,\n            encipher_only=False,\n            decipher_only=False,\n        ),\n        critical=False,\n    )\n\n    return _finalize(builder, pubkey=_NONROOT_PUBKEY)\n\n\n# Individual testcases; see each function's docstring.\n_dump(bogus_root(), _HERE / \"bogus-root.pem\")\n_dump(bogus_root_noncritical_bc(), _HERE / \"bogus-root-noncritical-bc.pem\")\n_dump(bogus_root_missing_ku(), _HERE / \"bogus-root-missing-ku.pem\")\n_dump(bogus_root_invalid_ku(), _HERE / \"bogus-root-invalid-ku.pem\")\n_dump(bogus_intermediate(), _HERE / \"bogus-intermediate.pem\")\n_dump(bogus_intermediate_with_eku(), _HERE / \"bogus-intermediate-with-eku.pem\")\n_dump(bogus_leaf(), _HERE / \"bogus-leaf.pem\")\n_dump(bogus_leaf_invalid_ku(), _HERE / \"bogus-leaf-invalid-ku.pem\")\n_dump(bogus_leaf_invalid_eku(), _HERE / \"bogus-leaf-invalid-eku.pem\")\n_dump(bogus_leaf_missing_eku(), _HERE / \"bogus-leaf-missing-eku.pem\")\n"
  },
  {
    "path": "test/assets/x509/nonroot-privkey.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC402QfBePPiHp0\nbYH6D9w2K+z1lWHQc1YoSPEd1JplDYJdWOcf3WsSPslOwRktiwgK9aOWVgsMJ86Z\nXyV/A5MVw8BSrKxoDtIXVsf/wqpMB9piu+tVBowXE7E59yWYwFzZw3Ya1ysstmud\nm7BnjQTtlyHHm4JnueO0pXYkvRxAimOz6uc1XGrLXnz9B/8Bx4Llz/mEjRYj2bD2\n4vufMbiFKb8Ja93t+9ALpRX9C30Giy15UhUH/ehh+y2fCGv/xqma+rTPr2I5/3/B\nQY9spamBrAXSX7ZnzEx+8Hbl3j2tdYi3LC14GWQ9WIT57DYjHtQBBnnkQ9nOGEjs\n+jOi7+ApAgMBAAECggEAN7zoUMLB9PA/naT4saTe0CdnCpjGKsrdjMCSlmBrP1ZX\nnjcVXHK1u4bbxrhNE4L+Je/2KXxBUKUglPgwoqE9Vi72bPhN9gOiMA+nuOXH3a3w\nmh351mZnEP6LT+PMnshEOBfOIkIJby6EPb+Z72CDv/L36O5o4UcZ+Hx9qI6vWnbe\nDhRpVuyp2Zbw6XOBSu5MWIpWjSBaJmaV2D5ad/EDC/XmRG+gMDHD1G3Hj0Mjlq1F\nn4G94UJwqWGwaaQGjE6gKino/ecbEmaOl4KGKrcdU0wZodtxdN8q54qthQ4z48iu\nPTWQQSPgUm9OjnWh/Qg3KKQxuY7Hyfn9Lf0TywPj0QKBgQDqxJA2pj8542foJQyj\nPaF29awaKwYpK7IFpLbUAer7kw9P0UWFiOpSQWLHecW0J0YyQjOynS0/FbyCgcNF\n4JxoVaBJGKyR1bGuMVD/u12RgfXktyLW+e3jEPoBqoIuK5AFMamT+nxT0WBjRJ0c\noVdsFDol5pXUfKpP7btpTIiUgwKBgQDJioyXcGpuiE8lfvbrQI2TO4rv4m1J1Nyk\npwl1MrUBesT3+JrB4pT9AqknBN6koenknY7ZVlhvHbAPbgnSi7HW1xJcsSew7Dxl\nqgnFj26kEMptZjHlzELTPr34RCvP23iUfb2yiEj0kbFYMOBsu7oxC6+lT+XbIkIZ\nbmc7Y4QQ4wKBgQCj3lpPWxGM5ZeUqa+9jfpTX74mcduWB0L2v3dCWqhbu9WXQBrH\nz77HdY5ucCg4zKUp1Z3iUeXQP+raKZtU/igOh54fB5MFJGUmkpPYPT9dnpo1cENo\nTQHoWeQ4H31Inu2jQnv8p336v44JHE6SOmgcL6464E27CN2Udvs2z84R4wKBgCfh\nKoCs1eKZRk/9F47lbx47Ifrlqwp4/E/4XX67UeXBDUikALtswl5uMFpwND4Pa+C4\n7JNE6qrSDQyAkaD/02jXleKRi3EOzcSwKM7W2uXMDMIo/qaiDHcQaza9Bo5St0Fq\nwCaboRQD4Du7MC1T2DvsPA1SCgGafcnadsLhpjhRAoGBAM2ofL3gyu9/hSzmlcCf\nnhnrE3ccqH5ln9//LnuzMsaqFWFG9zhNrUYuujkzxeaF9P+0sXfLjHnC9yNlO2qa\noyoqySXDy0Kc0q7iVF9XHrv0KMt4KO0KQ6XH3SfM3+0SssSMwyG0M09H5Lh+3GP7\nQTjwn+hqS4k6vFLZ4HHy+qQi\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "test/assets/x509/root-privkey.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJGnUWsZb/91FD\nwpxo4z2JoPZVQnDj8fnXcm7EL/tpD7tY98EtDjAvK2gkf0Tt5JOsA+OHuWKVthW8\nvZMUsImSJoa46C86kI3y7EL2J7+v66L5iB7EnCUyPDF+rHrtZNdkm0L8ZVLjkhJ+\nroyQjBS1Ba9M9cmceXZ6khePh6XpchBZAv+SgGBSXNs2t93BnU2kCbSM0MrjgONc\nMLATLxSVMMyV7V0BvhXzQaUXmzztxq42nmx+CHPTmKpmJNKDjjrW/8QUftMSUC25\nouprfo9ujU4QiR5bJ6PyfrSYVvBja/gWtv0b29AxjWBXu8u1618mmAooiP+Ir+Nu\nyuuGOwJHAgMBAAECggEAFZogdWbFNCCycNzA+r6yN7b7zwPF5vkcConHHo7oQDcJ\nkRB9W+QixpEF7P8OKJ8S7SQ2duKx6tK2OgyDy0dSt8l9/kh+o5lZ43wqjeY41Tey\nzYAoN0bDSMamKxfG//otmFKEmw0dSXHBnSxjJWHOwEqTM+lRFgcGyZAo32jwUweW\nD8cjuYmoL87I6xMmmcAnSjhTv51inboAoo/PK1OHVX7rcmZ5N61/zEF2swo+wzsm\nXkgVQcGa6tjstT85/c6KFx8yZju/Na8jqzpTJS1o11QUy86t9VHF8qNyOrzFrTQr\nEBP2kp5QCrCsyy4yk9n/bdKY6BR8h1lhS6yNCvBHSQKBgQDnvtUVwJLy5fvbbexc\nf7+7kIVPAGEJAfb5ZYsg82sECKCEyg4TSMxzWPw30XVrdC9+tqYRGk4AO77hdEz9\nYUCs2KSQU22Ve50oqRP+I2vkTQWbdmDW+saB0+HHjaPG5Dwt/pdPJwlloqUgHSXr\nJ2KOAjzs4qsirGb8LN4LDiM0nwKBgQDeJqFBYRlJd1mSlLArrVHFmm/0dQTeHS2h\nxYOll6iSxPrd6++9FuCsCTdAnLpA0V8gRY7z/jKWY8CQbAYtLvBAz2Sn+kaHXNSn\n/pzWRl4sMSnfa16GZWz46m8NBhTdUGdA94Wj3LqDTFDiXwwYRwuPtewK820ZObh5\n+vD6Z0fpWQKBgQCMMh84lJKRjV5LBfnqj4IPV0O+Yk1RpLWjdLGxUnEYNJvfGVlg\ngzbkRR34KqftRJGDB735NL+hVoOIYtI8qvv0VO9hPIdb2jdeJMMqiIU5zPqqbPfy\nti0m12aMUXyV0vcxIAarZMNDkBxzDA8nbmEp5eKzsAC17jQzNHVznK7howKBgBco\nj8bxCGHQP1Y4ieUDvHKNFv609Dzzbb5fiMnKdZhXUI+x+NwNdn54t3nU3NXE/dWv\naqek6EElRP3JRRuQuRsIg8W/IXsbAlBBCriLvWV9+o9/8eqwyBtq1QjWiXZI23q6\nUwQyDn+BhS0UG36saVgh7ul1Vvo6OjD9KAHyolyBAoGAIgncKn3cytyeub8WYzPh\nOU8DlAuiwMylMrOtBASDSgnx3zVefyGUqSXh8wK92MgZAiBYI4PaHymbil3PMC0A\n1PT8f2Cen70L2aYMDt4+8c5arT6v8bGwtdz/BKjZTca3nblU4nnhhd2aY5e3Ut3d\nffkc8EfGnIeo7hZBqKAe0gY=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "test/conftest.py",
    "content": "# Copyright 2024 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport os\nfrom pathlib import Path\n\nimport pytest\nfrom id import (\n    AmbientCredentialError,\n    GitHubOidcPermissionCredentialError,\n    detect_credential,\n)\n\n_ASSETS = (Path(__file__).parent / \"assets\").resolve()\nassert _ASSETS.is_dir()\n\nTEST_CLIENT_ID = \"sigstore\"\n\n\n@pytest.fixture\ndef asset():\n    def _asset(name: str) -> Path:\n        return _ASSETS / name\n\n    return _asset\n\n\ndef _has_oidc_id():\n    # If there are tokens manually defined for us in the environment, use them.\n    if os.getenv(\"SIGSTORE_IDENTITY_TOKEN_production\") or os.getenv(\n        \"SIGSTORE_IDENTITY_TOKEN_staging\"\n    ):\n        return True\n\n    try:\n        token = detect_credential(TEST_CLIENT_ID)\n        if token is None:\n            return False\n    except GitHubOidcPermissionCredentialError:\n        # On GitHub Actions, forks do not have access to OIDC identities.\n        # We differentiate this case from other GitHub credential errors,\n        # since it's a case where we want to skip (i.e. return False).\n        #\n        # We also skip when the repo isn't our own, since downstream\n        # regression testers (e.g. PyCA Cryptography) don't necessarily\n        # want to give our unit tests access to an OIDC identity.\n        return (\n            os.getenv(\"GITHUB_REPOSITORY\") == \"sigstore/sigstore-python\"\n            and os.getenv(\"GITHUB_EVENT_NAME\") != \"pull_request\"\n        )\n    except AmbientCredentialError:\n        # If ambient credential detection raises, then we *are* in an ambient\n        # environment but one that's been configured incorrectly. We\n        # pass this through, so that the CI fails appropriately rather than\n        # silently skipping the faulty tests.\n        return True\n\n    return True\n\n\ndef _has_timestamp_authority_configured() -> bool:\n    \"\"\"\n    Check if there is a Timestamp Authority that has been configured\n    \"\"\"\n    return os.getenv(\"TEST_SIGSTORE_TIMESTAMP_AUTHORITY_URL\") is not None\n\n\ndef pytest_addoption(parser):\n    parser.addoption(\n        \"--skip-online\",\n        action=\"store_true\",\n        help=\"skip tests that require network connectivity\",\n    )\n    parser.addoption(\n        \"--skip-staging\",\n        action=\"store_true\",\n        help=\"skip tests that require Sigstore staging infrastructure\",\n    )\n\n\ndef pytest_runtest_setup(item):\n    # Do we need a network connection?\n    online = False\n    for mark in [\"online\", \"staging\", \"production\"]:\n        if mark in item.keywords:\n            online = True\n\n    if online and item.config.getoption(\"--skip-online\"):\n        pytest.skip(\n            \"skipping test that requires network connectivity due to `--skip-online` flag\"\n        )\n    elif \"ambient_oidc\" in item.keywords and not _has_oidc_id():\n        pytest.skip(\"skipping test that requires an ambient OIDC credential\")\n\n    if \"staging\" in item.keywords and item.config.getoption(\"--skip-staging\"):\n        pytest.skip(\n            \"skipping test that requires staging infrastructure due to `--skip-staging` flag\"\n        )\n\n    if (\n        \"timestamp_authority\" in item.keywords\n        and not _has_timestamp_authority_configured()\n    ):\n        pytest.skip(\"skipping test that requires a Timestamp Authority\")\n\n\ndef pytest_configure(config):\n    config.addinivalue_line(\n        \"markers\", \"staging: mark test as requiring Sigstore staging infrastructure\"\n    )\n    config.addinivalue_line(\n        \"markers\",\n        \"production: mark test as requiring Sigstore production infrastructure\",\n    )\n    config.addinivalue_line(\n        \"markers\",\n        \"online: mark test as requiring network connectivity (but not a specific Sigstore infrastructure)\",\n    )\n    config.addinivalue_line(\n        \"markers\", \"ambient_oidc: mark test as requiring an ambient OIDC identity\"\n    )\n    config.addinivalue_line(\n        \"markers\", \"timestamp_authority: mark test as requiring a timestamp authority\"\n    )\n"
  },
  {
    "path": "test/integration/cli/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n"
  },
  {
    "path": "test/integration/cli/conftest.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nfrom collections.abc import Callable\nfrom pathlib import Path\n\nimport pytest\n\nfrom sigstore._cli import main\n\n\n@pytest.fixture\ndef asset_integration(asset):\n    def _asset(name: str) -> Path:\n        return asset(f\"integration/{name}\")\n\n    return _asset\n\n\n@pytest.fixture(scope=\"function\")\ndef sigstore() -> Callable:\n    def _sigstore(*args: str):\n        main(list(args))\n\n    return _sigstore\n"
  },
  {
    "path": "test/integration/cli/test_attest.py",
    "content": "# Copyright 2024 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nfrom pathlib import Path\n\nimport pytest\n\nfrom sigstore.dsse._predicate import PredicateType\nfrom sigstore.models import Bundle\nfrom sigstore.verify import Verifier\nfrom sigstore.verify.policy import UnsafeNoOp\n\n\ndef get_cli_params(\n    pred_type: str,\n    pred_path: Path,\n    artifact_path: Path,\n    overwrite: bool = False,\n    bundle_path: Path | None = None,\n) -> list[str]:\n    cli_params = [\n        \"--staging\",\n        \"attest\",\n        \"--predicate-type\",\n        pred_type,\n        \"--predicate\",\n        str(pred_path),\n    ]\n    if bundle_path is not None:\n        cli_params.extend([\"--bundle\", str(bundle_path)])\n    if overwrite:\n        cli_params.append(\"--overwrite\")\n    cli_params.append(str(artifact_path))\n\n    return cli_params\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\n@pytest.mark.parametrize(\n    (\"predicate_type\", \"predicate_filename\"),\n    [\n        (PredicateType.SLSA_v0_2, \"slsa_predicate_v0_2.json\"),\n        (PredicateType.SLSA_v1_0, \"slsa_predicate_v1_0.json\"),\n    ],\n)\ndef test_attest_success_default_output_bundle(\n    capsys, sigstore, asset_integration, predicate_type, predicate_filename\n):\n    predicate_path = asset_integration(f\"attest/{predicate_filename}\")\n    artifact = asset_integration(\"a.txt\")\n    expected_output_bundle = artifact.with_name(\"a.txt.sigstore.json\")\n\n    assert not expected_output_bundle.exists()\n    sigstore(\n        *get_cli_params(\n            pred_type=predicate_type,\n            pred_path=predicate_path,\n            artifact_path=artifact,\n        )\n    )\n\n    assert expected_output_bundle.exists()\n    verifier = Verifier.staging()\n    with open(expected_output_bundle, \"r\", encoding=\"utf-8\") as bundle_file:\n        bundle = Bundle.from_json(bundle_file.read())\n        verifier.verify_dsse(bundle=bundle, policy=UnsafeNoOp())\n\n    expected_output_bundle.unlink()\n\n    captures = capsys.readouterr()\n    assert captures.out.endswith(\n        f\"Sigstore bundle written to {expected_output_bundle}\\n\"\n    )\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_attest_success_custom_output_bundle(\n    capsys, sigstore, asset_integration, tmp_path\n):\n    predicate_type = PredicateType.SLSA_v0_2\n    predicate_filename = \"slsa_predicate_v0_2.json\"\n    predicate_path = asset_integration(f\"attest/{predicate_filename}\")\n    artifact = asset_integration(\"a.txt\")\n\n    output_bundle = tmp_path / \"bundle.json\"\n    assert not output_bundle.exists()\n    sigstore(\n        *get_cli_params(\n            pred_type=predicate_type,\n            pred_path=predicate_path,\n            artifact_path=artifact,\n            bundle_path=output_bundle,\n        )\n    )\n\n    assert output_bundle.exists()\n    captures = capsys.readouterr()\n    assert captures.out.endswith(f\"Sigstore bundle written to {output_bundle}\\n\")\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_attest_overwrite_existing_bundle(\n    capsys, sigstore, asset_integration, tmp_path\n):\n    predicate_type = PredicateType.SLSA_v0_2\n    predicate_filename = \"slsa_predicate_v0_2.json\"\n    predicate_path = asset_integration(f\"attest/{predicate_filename}\")\n    artifact = asset_integration(\"a.txt\")\n\n    output_bundle = tmp_path / \"bundle.json\"\n    assert not output_bundle.exists()\n\n    cli_params = get_cli_params(\n        pred_type=predicate_type,\n        pred_path=predicate_path,\n        artifact_path=artifact,\n        bundle_path=output_bundle,\n    )\n    sigstore(*cli_params)\n    assert output_bundle.exists()\n\n    # On invalid argument errors we call `Argumentparser.error`, which prints\n    # a message and exits with code 2\n    with pytest.raises(SystemExit) as e:\n        sigstore(*cli_params)\n    assert e.value.code == 2\n\n    assert output_bundle.exists()\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        f\"Refusing to overwrite outputs without --overwrite: {output_bundle}\\n\"\n    )\n\n    cli_params.append(\"--overwrite\")\n    sigstore(*cli_params)\n    assert output_bundle.exists()\n\n    assert captures.out.endswith(f\"Sigstore bundle written to {output_bundle}\\n\")\n\n\ndef test_attest_invalid_predicate_type(capsys, sigstore, asset_integration, tmp_path):\n    predicate_type = \"invalid_type\"\n    predicate_filename = \"slsa_predicate_v0_2.json\"\n    predicate_path = asset_integration(f\"attest/{predicate_filename}\")\n    artifact = asset_integration(\"a.txt\")\n\n    output_bundle = tmp_path / \"bundle.json\"\n    # On invalid argument errors we call `Argumentparser.error`, which prints\n    # a message and exits with code 2\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                pred_type=predicate_type,\n                pred_path=predicate_path,\n                artifact_path=artifact,\n                bundle_path=output_bundle,\n            )\n        )\n    assert e.value.code == 2\n\n    captures = capsys.readouterr()\n    assert captures.err.endswith(f\"invalid PredicateType value: '{predicate_type}'\\n\")\n\n\ndef test_attest_mismatching_predicate(capsys, sigstore, asset_integration, tmp_path):\n    predicate_type = PredicateType.SLSA_v0_2\n    predicate_filename = \"slsa_predicate_v1_0.json\"\n    predicate_path = asset_integration(f\"attest/{predicate_filename}\")\n    artifact = asset_integration(\"a.txt\")\n\n    output_bundle = tmp_path / \"bundle.json\"\n    # On invalid argument errors we call `Argumentparser.error`, which prints\n    # a message and exits with code 2\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                pred_type=predicate_type,\n                pred_path=predicate_path,\n                artifact_path=artifact,\n                bundle_path=output_bundle,\n            )\n        )\n    assert e.value.code == 2\n\n    captures = capsys.readouterr()\n    assert f'Unable to parse predicate of type \"{predicate_type}\":' in captures.err\n\n\ndef test_attest_missing_predicate(capsys, sigstore, asset_integration, tmp_path):\n    predicate_type = PredicateType.SLSA_v0_2\n    predicate_filename = \"doesnt_exist.json\"\n    predicate_path = asset_integration(f\"attest/{predicate_filename}\")\n    artifact = asset_integration(\"a.txt\")\n\n    output_bundle = tmp_path / \"bundle.json\"\n    # On invalid argument errors we call `Argumentparser.error`, which prints\n    # a message and exits with code 2\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                pred_type=predicate_type,\n                pred_path=predicate_path,\n                artifact_path=artifact,\n                bundle_path=output_bundle,\n            )\n        )\n    assert e.value.code == 2\n\n    captures = capsys.readouterr()\n    assert captures.err.endswith(f\"Predicate must be a file: {predicate_path}\\n\")\n\n\ndef test_attest_invalid_json_predicate(capsys, sigstore, asset_integration, tmp_path):\n    predicate_type = PredicateType.SLSA_v0_2\n    predicate_path = asset_integration(\"a.txt\")\n    artifact = asset_integration(\"a.txt\")\n\n    output_bundle = tmp_path / \"bundle.json\"\n    # On invalid argument errors we call `Argumentparser.error`, which prints\n    # a message and exits with code 2\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                pred_type=predicate_type,\n                pred_path=predicate_path,\n                artifact_path=artifact,\n                bundle_path=output_bundle,\n            )\n        )\n    assert e.value.code == 2\n\n    captures = capsys.readouterr()\n    assert f'Unable to parse predicate of type \"{predicate_type}\":' in captures.err\n"
  },
  {
    "path": "test/integration/cli/test_plumbing.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\nimport pytest\nfrom sigstore_models.common.v1 import HashAlgorithm\n\nfrom sigstore.hashes import Hashed\nfrom sigstore.models import Bundle, InvalidBundle\nfrom sigstore.verify import policy\nfrom sigstore.verify.verifier import Verifier\n\n\ndef test_fix_bundle_fixes_missing_checkpoint(capsys, sigstore, asset_integration):\n    invalid_bundle = asset_integration(\"Python-3.12.5.tgz.sigstore\")\n\n    # The bundle is invalid, because it's missing a checkpoint\n    # for its inclusion proof.\n    with pytest.raises(\n        InvalidBundle, match=\"entry must contain inclusion proof, with checkpoint\"\n    ):\n        Bundle.from_json(invalid_bundle.read_text())\n\n    # Running `sigstore plumbing fix-bundle` emits a fixed bundle.\n    sigstore(\"plumbing\", \"fix-bundle\", \"--bundle\", str(invalid_bundle))\n\n    captures = capsys.readouterr()\n\n    # The bundle now loads correctly.\n    bundle = Bundle.from_json(captures.out)\n\n    # We didn't pass `--upgrade-version` so the version is still v0.1.\n    assert bundle._inner.media_type == Bundle.BundleType.BUNDLE_0_1\n\n    # ...and the fixed bundle can now be used to verify the `Python-3.12.5.tgz`\n    # release.\n    verifier = Verifier.production()\n    verifier.verify_artifact(\n        Hashed(\n            algorithm=HashAlgorithm.SHA2_256,\n            digest=bytes.fromhex(\n                \"38dc4e2c261d49c661196066edbfb70fdb16be4a79cc8220c224dfeb5636d405\"\n            ),\n        ),\n        bundle,\n        policy.AllOf(\n            [\n                policy.Identity(\n                    identity=\"thomas@python.org\", issuer=\"https://accounts.google.com\"\n                )\n            ]\n        ),\n    )\n\n\ndef test_fix_bundle_upgrades_bundle(capsys, sigstore, asset_integration):\n    invalid_bundle = asset_integration(\"Python-3.12.5.tgz.sigstore\")\n\n    # Running `sigstore plumbing fix-bundle --upgrade-version`\n    # emits a fixed bundle.\n    sigstore(\n        \"plumbing\", \"fix-bundle\", \"--upgrade-version\", \"--bundle\", str(invalid_bundle)\n    )\n\n    captures = capsys.readouterr()\n\n    # The bundle now loads correctly.\n    bundle = Bundle.from_json(captures.out)\n\n    # The bundle is now the latest version (v0.3).\n    assert bundle._inner.media_type == Bundle.BundleType.BUNDLE_0_3\n\n    # ...and the upgraded (and fixed) bundle can still verify\n    # the release.\n    # ...and the fixed can now be used to verify the `Python-3.12.5.tgz` release.\n    verifier = Verifier.production()\n    verifier.verify_artifact(\n        Hashed(\n            algorithm=HashAlgorithm.SHA2_256,\n            digest=bytes.fromhex(\n                \"38dc4e2c261d49c661196066edbfb70fdb16be4a79cc8220c224dfeb5636d405\"\n            ),\n        ),\n        bundle,\n        policy.AllOf(\n            [\n                policy.Identity(\n                    identity=\"thomas@python.org\", issuer=\"https://accounts.google.com\"\n                )\n            ]\n        ),\n    )\n"
  },
  {
    "path": "test/integration/cli/test_sign.py",
    "content": "# Copyright 2024 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nfrom pathlib import Path\n\nimport pytest\n\nfrom sigstore.models import Bundle\nfrom sigstore.verify import Verifier\nfrom sigstore.verify.policy import UnsafeNoOp\n\n\ndef get_cli_params(\n    artifact_paths: list[Path],\n    overwrite: bool = False,\n    no_default_files: bool = False,\n    output_directory: Path | None = None,\n    bundle_path: Path | None = None,\n    signature_path: Path | None = None,\n    certificate_path: Path | None = None,\n    trust_config_path: Path | None = None,\n) -> list[str]:\n    if trust_config_path is not None:\n        cli_params = [\"--trust-config\", str(trust_config_path), \"sign\"]\n    else:\n        cli_params = [\"--staging\", \"sign\"]\n\n    if output_directory is not None:\n        cli_params.extend([\"--output-directory\", str(output_directory)])\n    if bundle_path is not None:\n        cli_params.extend([\"--bundle\", str(bundle_path)])\n    if signature_path is not None:\n        cli_params.extend([\"--signature\", str(signature_path)])\n    if certificate_path is not None:\n        cli_params.extend([\"--certificate\", str(certificate_path)])\n    if overwrite:\n        cli_params.append(\"--overwrite\")\n    if no_default_files:\n        cli_params.append(\"--no-default-files\")\n\n    cli_params.extend([str(p) for p in artifact_paths])\n\n    return cli_params\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_success_default_output_bundle(\n    capsys, sigstore, asset_integration, tmp_path\n):\n    artifact = asset_integration(\"a.txt\")\n    expected_output_bundle = tmp_path / \"a.txt.sigstore.json\"\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=[artifact],\n            output_directory=tmp_path,\n        )\n    )\n\n    assert expected_output_bundle.exists()\n    verifier = Verifier.staging()\n    with (\n        open(expected_output_bundle, \"r\", encoding=\"utf-8\") as bundle_file,\n        open(artifact, \"rb\") as input_file,\n    ):\n        bundle = Bundle.from_json(bundle_file.read())\n        verifier.verify_artifact(\n            input_=input_file.read(), bundle=bundle, policy=UnsafeNoOp()\n        )\n\n    captures = capsys.readouterr()\n    assert captures.out.endswith(\n        f\"Sigstore bundle written to {expected_output_bundle}\\n\"\n    )\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_success_multiple_artifacts(capsys, sigstore, asset_integration, tmp_path):\n    artifacts: list[Path] = [\n        asset_integration(\"a.txt\"),\n        asset_integration(\"b.txt\"),\n        asset_integration(\"c.txt\"),\n    ]\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=artifacts,\n            output_directory=tmp_path,\n        )\n    )\n\n    captures = capsys.readouterr()\n\n    for artifact in artifacts:\n        expected_output_bundle = tmp_path / f\"{artifact.name}.sigstore.json\"\n\n        assert f\"Sigstore bundle written to {expected_output_bundle}\\n\" in captures.out\n\n        assert expected_output_bundle.exists()\n        verifier = Verifier.staging()\n        with (\n            open(expected_output_bundle, \"r\", encoding=\"utf-8\") as bundle_file,\n            open(artifact, \"rb\") as input_file,\n        ):\n            bundle = Bundle.from_json(bundle_file.read())\n            verifier.verify_artifact(\n                input_=input_file.read(), bundle=bundle, policy=UnsafeNoOp()\n            )\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_success_multiple_artifacts_rekor_v2(\n    capsys, sigstore, asset_integration, asset, tmp_path\n):\n    \"\"\"This is a copy of test_sign_success_multiple_artifacts that exists to ensure the\n    multi-threaded signing works with rekor v2 as well: this test can be removed when v2\n    is the default\n    \"\"\"\n\n    artifacts: list[Path] = [\n        asset_integration(\"a.txt\"),\n        asset_integration(\"b.txt\"),\n        asset_integration(\"c.txt\"),\n    ]\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=artifacts,\n            output_directory=tmp_path,\n        )\n    )\n\n    captures = capsys.readouterr()\n\n    for artifact in artifacts:\n        expected_output_bundle = tmp_path / f\"{artifact.name}.sigstore.json\"\n\n        assert f\"Sigstore bundle written to {expected_output_bundle}\\n\" in captures.out\n\n        assert expected_output_bundle.exists()\n        verifier = Verifier.staging()\n        with (\n            open(expected_output_bundle, \"r\", encoding=\"utf-8\") as bundle_file,\n            open(artifact, \"rb\") as input_file,\n        ):\n            bundle = Bundle.from_json(bundle_file.read())\n            verifier.verify_artifact(\n                input_=input_file.read(), bundle=bundle, policy=UnsafeNoOp()\n            )\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_success_custom_outputs(capsys, sigstore, asset_integration, tmp_path):\n    artifact = asset_integration(\"a.txt\")\n    output_bundle = tmp_path / \"bundle.json\"\n    output_cert = tmp_path / \"cert.cert\"\n    output_signature = tmp_path / \"signature.sig\"\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=[artifact],\n            bundle_path=output_bundle,\n            certificate_path=output_cert,\n            signature_path=output_signature,\n        )\n    )\n\n    assert output_bundle.exists()\n    assert output_cert.exists()\n    assert output_signature.exists()\n\n    captures = capsys.readouterr()\n    assert captures.out.endswith(\n        f\"Signature written to {output_signature}\\nCertificate written to {output_cert}\\nSigstore bundle written to {output_bundle}\\n\"\n    )\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_success_custom_output_dir(capsys, sigstore, asset_integration, tmp_path):\n    artifact = asset_integration(\"a.txt\")\n    expected_output_bundle = tmp_path / \"a.txt.sigstore.json\"\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=[artifact],\n            output_directory=tmp_path,\n        )\n    )\n\n    assert expected_output_bundle.exists()\n\n    captures = capsys.readouterr()\n    assert captures.out.endswith(\n        f\"Sigstore bundle written to {expected_output_bundle}\\n\"\n    )\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_success_no_default_files(capsys, sigstore, asset_integration, tmp_path):\n    artifact = asset_integration(\"a.txt\")\n    default_output_bundle = tmp_path / \"a.txt.sigstore.json\"\n    output_cert = tmp_path / \"cert.cert\"\n    output_signature = tmp_path / \"sig.sig\"\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=[artifact],\n            signature_path=output_signature,\n            certificate_path=output_cert,\n            no_default_files=True,\n        )\n    )\n    assert output_cert.exists()\n    assert output_signature.exists()\n    assert not default_output_bundle.exists()\n\n    captures = capsys.readouterr()\n    assert captures.out.endswith(\n        f\"Signature written to {output_signature}\\nCertificate written to {output_cert}\\n\"\n    )\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_overwrite_existing_bundle(capsys, sigstore, asset_integration, tmp_path):\n    artifact = asset_integration(\"a.txt\")\n    expected_output_bundle = tmp_path / \"a.txt.sigstore.json\"\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=[artifact],\n            output_directory=tmp_path,\n        )\n    )\n\n    assert expected_output_bundle.exists()\n\n    sigstore(\n        *get_cli_params(\n            artifact_paths=[artifact],\n            output_directory=tmp_path,\n            overwrite=True,\n        )\n    )\n    assert expected_output_bundle.exists()\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact],\n                output_directory=tmp_path,\n                overwrite=False,\n            )\n        )\n    assert e.value.code == 2\n\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        f\"Refusing to overwrite outputs without --overwrite: {expected_output_bundle}\\n\"\n    )\n\n\ndef test_sign_fails_with_default_files_and_bundle_options(\n    capsys, sigstore, asset_integration\n):\n    artifact = asset_integration(\"a.txt\")\n    output_bundle = artifact.with_name(\"a.txt.sigstore.json\")\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact],\n                bundle_path=output_bundle,\n                no_default_files=True,\n            )\n        )\n    assert e.value.code == 2\n\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"--no-default-files may not be combined with --bundle.\\n\"\n    )\n\n\ndef test_sign_fails_with_multiple_inputs_and_custom_output(\n    capsys, sigstore, asset_integration\n):\n    artifact = asset_integration(\"a.txt\")\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact, artifact],\n                bundle_path=artifact.with_name(\"a.txt.sigstore.json\"),\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\\n\"\n    )\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact, artifact],\n                certificate_path=artifact.with_name(\"a.txt.cert\"),\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\\n\"\n    )\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact, artifact],\n                signature_path=artifact.with_name(\"a.txt.sig\"),\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\\n\"\n    )\n\n\ndef test_sign_fails_with_output_dir_and_custom_output_files(\n    capsys, sigstore, asset_integration\n):\n    artifact = asset_integration(\"a.txt\")\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact],\n                bundle_path=artifact.with_name(\"a.txt.sigstore.json\"),\n                output_directory=artifact.parent,\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\\n\"\n    )\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact],\n                certificate_path=artifact.with_name(\"a.txt.cert\"),\n                output_directory=artifact.parent,\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\\n\"\n    )\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact],\n                signature_path=artifact.with_name(\"a.txt.sig\"),\n                output_directory=artifact.parent,\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\\n\"\n    )\n\n\ndef test_sign_fails_without_both_output_cert_and_signature(\n    capsys, sigstore, asset_integration\n):\n    artifact = asset_integration(\"a.txt\")\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact],\n                certificate_path=artifact.with_name(\"a.txt.cert\"),\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature and --certificate must be used together.\\n\"\n    )\n\n    with pytest.raises(SystemExit) as e:\n        sigstore(\n            *get_cli_params(\n                artifact_paths=[artifact],\n                signature_path=artifact.with_name(\"a.txt.sig\"),\n            )\n        )\n    assert e.value.code == 2\n    captures = capsys.readouterr()\n    assert captures.err.endswith(\n        \"Error: --signature and --certificate must be used together.\\n\"\n    )\n"
  },
  {
    "path": "test/integration/cli/test_verify.py",
    "content": "# Copyright 2024 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport pytest\n\n\n@pytest.mark.staging\ndef test_regression_verify_legacy_bundle(capsys, caplog, asset_integration, sigstore):\n    # Check that verification continues to work when legacy bundle is present (*.sigstore) and\n    # no cert, sig and normal bundle (*.sigstore.json) are present.\n    artifact_filename = \"bundle_v3.txt\"\n    artifact = asset_integration(artifact_filename)\n    legacy_bundle = asset_integration(f\"{artifact_filename}.sigstore\")\n\n    sig = asset_integration(f\"{artifact_filename}.sig\")\n    cert = asset_integration(f\"{artifact_filename}.crt\")\n    bundle = asset_integration(f\"{artifact_filename}.sigstore.json\")\n    assert not cert.is_file()\n    assert not sig.is_file()\n    assert not bundle.is_file()\n\n    sigstore(\n        \"--staging\",\n        \"verify\",\n        \"identity\",\n        str(artifact),\n        \"--cert-identity\",\n        \"william@yossarian.net\",\n        \"--cert-oidc-issuer\",\n        \"https://github.com/login/oauth\",\n    )\n\n    captures = capsys.readouterr()\n    assert captures.err == f\"OK: {artifact.absolute()}\\n\"\n\n    assert len(caplog.records) == 1\n    assert (\n        caplog.records[0].message\n        == f\"{artifact.absolute()}: {legacy_bundle.absolute()} should be named {bundle.absolute()}. Support for discovering 'bare' .sigstore inputs will be deprecated in a future release.\"\n    )\n"
  },
  {
    "path": "test/integration/sigstore-python-conformance",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nA wrapper to convert `sigstore-conformance` CLI protocol invocations to match `sigstore-python`.\n\"\"\"\n\nimport json\nimport os\nimport sys\nfrom contextlib import suppress\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\n\n# The signing config in this trust_config is not used: it's just here\n# so the built trustconfig is complete\ntrust_config = {\n    \"mediaType\": \"application/vnd.dev.sigstore.clienttrustconfig.v0.1+json\",\n    \"signingConfig\": {\n        \"mediaType\": \"application/vnd.dev.sigstore.signingconfig.v0.2+json\",\n        \"caUrls\": [{\n            \"url\": \"https://fulcio.example.com\",\n            \"majorApiVersion\": 1,\n            \"operator\": \"\",\n            \"validFor\": {\"start\": \"1970-01-01T01:01:01Z\"}\n        }],\n        \"oidcUrls\": [],\n        \"rekorTlogUrls\": [{\n            \"url\": \"https://rekor.example.com\",\n            \"majorApiVersion\": 1,\n            \"operator\": \"\",\n            \"validFor\": {\"start\": \"1970-01-01T01:01:01Z\"}\n        }],\n        \"tsaUrls\": [],\n        \"rekorTlogConfig\": {\"selector\": \"ANY\"},\n        \"tsaConfig\": {\"selector\": \"ANY\"},\n    },\n}\n\nSUBCMD_REPLACEMENTS = {\n    \"sign-bundle\": \"sign\",\n    \"verify-bundle\": \"verify\",\n}\n\nARG_REPLACEMENTS = {\n    \"--certificate-identity\": \"--cert-identity\",\n    \"--certificate-oidc-issuer\": \"--cert-oidc-issuer\",\n}\n\n# Trim the script name.\nfixed_args = sys.argv[1:]\n\n# Substitute incompatible subcommands.\nsubcmd = fixed_args[0]\nif subcmd in SUBCMD_REPLACEMENTS:\n    fixed_args[0] = SUBCMD_REPLACEMENTS[subcmd]\n\n# Build base command with optional staging argument\ncommand = [\"sigstore\"]\nif \"--staging\" in fixed_args:\n    command.append(\"--staging\")\n    fixed_args.remove(\"--staging\")\n\n# We may get \"--trusted-root\" and \"--signing-config\" as argument but sigstore-python\n# wants \"--trust-config\":\ntrusted_root_path = None\nwith suppress(ValueError):\n    i = fixed_args.index(\"--trusted-root\")\n    trusted_root_path = fixed_args[i + 1]\n    fixed_args.pop(i)\n    fixed_args.pop(i)\n\nsigning_config_path = None\nwith suppress(ValueError):\n    i = fixed_args.index(\"--signing-config\")\n    signing_config_path = fixed_args[i + 1]\n    fixed_args.pop(i)\n    fixed_args.pop(i)\n\n\n# If we did get a trustedroot, write a matching trustconfig into a temp file\n# Use given signingconfig if possible, otherwise use the fake one in template\nwith NamedTemporaryFile(mode=\"wt\") as temp_file:\n    if trusted_root_path is not None:\n        with open(trusted_root_path) as f:\n            trusted_root = json.load(f)\n        trust_config[\"trustedRoot\"] = trusted_root\n        if signing_config_path is not None:\n            with open(signing_config_path) as f:\n                signing_config = json.load(f)\n            trust_config[\"signingConfig\"] = signing_config\n\n        json.dump(trust_config, temp_file)\n        temp_file.flush()\n\n        command.extend([\"--trust-config\", temp_file.name])\n\n    # Fix-up the subcommand: the conformance suite uses `verify`, but\n    # `sigstore` requires `verify identity` for identity based verifications.\n    subcommand, *fixed_args = fixed_args\n    if subcommand == \"sign\":\n        if \"--in-toto\" in fixed_args:\n            # Handle DSSE signing via library call\n            fixed_args.remove(\"--in-toto\")\n\n            from sigstore.dsse import Statement\n            from sigstore.models import ClientTrustConfig\n            from sigstore.oidc import IdentityToken\n            from sigstore.sign import SigningContext\n\n            identity_token = None\n            bundle_path = None\n            try:\n                i = fixed_args.index(\"--identity-token\")\n                identity_token = fixed_args[i + 1]\n                i = fixed_args.index(\"--bundle\")\n                bundle_path = fixed_args[i + 1]\n            except (ValueError, IndexError):\n                raise ValueError(\"Missing required arguments for DSSE signing\")\n\n            # The statement is always the last argument in the protocol\n            input_file = fixed_args[-1]\n\n            # Direct library call\n            with open(input_file, \"rb\") as f:\n                statement_bytes = f.read()\n\n            statement = Statement(statement_bytes)\n\n            if trusted_root_path is not None:\n                trust_config_obj = ClientTrustConfig.from_json(Path(temp_file.name).read_text())\n            else:\n                trust_config_obj = ClientTrustConfig.production()\n                if \"--staging\" in sys.argv:\n                    trust_config_obj = ClientTrustConfig.staging()\n\n            context = SigningContext.from_trust_config(trust_config_obj)\n            token = IdentityToken(identity_token)\n\n            with context.signer(token) as signer:\n                bundle = signer.sign_dsse(statement)\n\n            with open(bundle_path, \"w\") as f:\n                f.write(bundle.to_json())\n\n            # Exit successfully after handling DSSE\n            sys.exit(0)\n        command.append(\"sign\")\n    elif subcommand == \"verify\":\n        command.extend([\"verify\", \"identity\"])\n    else:\n        raise ValueError(f\"unsupported subcommand: {subcommand}\")\n\n    # Replace incompatible flags.\n    command.extend(\n        ARG_REPLACEMENTS[arg] if arg in ARG_REPLACEMENTS else arg for arg in fixed_args\n    )\n\n    os.execvp(\"sigstore\", command)\n"
  },
  {
    "path": "test/unit/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n"
  },
  {
    "path": "test/unit/conftest.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom __future__ import annotations\n\nimport base64\nimport datetime\nimport os\nimport re\nfrom collections import defaultdict\nfrom collections.abc import Callable, Iterator\nfrom io import BytesIO\nfrom pathlib import Path\nfrom urllib.parse import urlparse\n\nimport jwt\nimport pytest\nfrom cryptography.x509 import Certificate, load_pem_x509_certificate\nfrom id import (\n    detect_credential,\n)\nfrom tuf.api.exceptions import DownloadHTTPError\nfrom tuf.ngclient import FetcherInterface, updater\n\nfrom sigstore._internal import tuf\nfrom sigstore._internal.rekor import _hashedrekord_from_parts\nfrom sigstore._internal.rekor.client import RekorClient\nfrom sigstore._utils import sha256_digest\nfrom sigstore.models import Bundle, ClientTrustConfig\nfrom sigstore.oidc import IdentityToken\nfrom sigstore.sign import SigningContext\nfrom sigstore.verify.verifier import Verifier\n\n_TUF_ASSETS = (Path(__file__).parent.parent / \"assets\" / \"staging-tuf\").resolve()\nassert _TUF_ASSETS.is_dir()\n\nTEST_CLIENT_ID = \"sigstore\"\n\n\n@pytest.fixture\ndef x509_testcase(asset):\n    def _x509_testcase(name: str) -> Certificate:\n        pem = asset(f\"x509/{name}\").read_bytes()\n        return load_pem_x509_certificate(pem)\n\n    return _x509_testcase\n\n\n@pytest.fixture\ndef tuf_asset():\n    SHA256_TARGET_PATTERN = re.compile(r\"[0-9a-f]{64}\\.\")\n\n    class TUFAsset:\n        def asset(self, name: str):\n            return (_TUF_ASSETS / name).read_bytes()\n\n        def target(self, name: str):\n            path = self.target_path(name)\n            return path.read_bytes() if path else None\n\n        def target_path(self, name: str) -> Path:\n            # Since TUF contains both sha256 and sha512 prefixed targets, filter\n            # out the sha512 ones.\n            matches = filter(\n                lambda path: SHA256_TARGET_PATTERN.match(path.name) is not None,\n                (_TUF_ASSETS / \"targets\").glob(f\"*.{name}\"),\n            )\n\n            try:\n                path = next(matches)\n            except StopIteration as e:\n                raise Exception(f\"Unable to match {name} in targets/\") from e\n\n            if next(matches, None) is None:\n                return path\n            return None\n\n    return TUFAsset()\n\n\n@pytest.fixture\ndef signing_materials(asset) -> Callable[[str, RekorClient], tuple[Path, Bundle]]:\n    # NOTE: Unlike `signing_bundle`, `signing_materials` requires a\n    # Rekor client to retrieve its entry with.\n    def _signing_materials(name: str, client: RekorClient) -> tuple[Path, Bundle]:\n        file = asset(name)\n        cert_path = asset(f\"{name}.crt\")\n        sig_path = asset(f\"{name}.sig\")\n\n        cert = load_pem_x509_certificate(cert_path.read_bytes())\n        sig = base64.b64decode(sig_path.read_text())\n        with file.open(mode=\"rb\") as io:\n            hashed = sha256_digest(io)\n\n        entry = client.log.entries.retrieve.post(\n            _hashedrekord_from_parts(cert, sig, hashed)\n        )\n\n        bundle = Bundle.from_parts(cert, sig, entry)\n\n        return (file, bundle)\n\n    return _signing_materials\n\n\n@pytest.fixture\ndef signing_bundle(asset) -> Callable[[str], tuple[Path, Bundle]]:\n    def _signing_bundle(name: str) -> tuple[Path, Bundle]:\n        file = asset(name)\n        bundle_path = asset(f\"{name}.sigstore\")\n        if not bundle_path.is_file():\n            bundle_path = asset(f\"{name}.sigstore.json\")\n        bundle = Bundle.from_json(bundle_path.read_bytes())\n\n        return (file, bundle)\n\n    return _signing_bundle\n\n\n@pytest.fixture\ndef null_policy():\n    class NullPolicy:\n        def verify(self, cert):\n            return\n\n    return NullPolicy()\n\n\n@pytest.fixture\ndef mock_staging_tuf(monkeypatch, tuf_dirs):\n    \"\"\"Mock that prevents python-tuf from making requests: it returns staging\n    assets from a local directory instead\n\n    Return a tuple of dicts with the requested files and counts\"\"\"\n\n    success = defaultdict(int)\n    failure = defaultdict(int)\n\n    class MockFetcher(FetcherInterface):\n        def _fetch(self, url: str) -> Iterator[bytes]:\n            filepath = _TUF_ASSETS / urlparse(url).path.lstrip(\"/\")\n            filename = filepath.name\n            if filepath.is_file():\n                success[filename] += 1\n                return BytesIO(filepath.read_bytes())\n\n            failure[filename] += 1\n            raise DownloadHTTPError(\"File not found\", 404)\n\n    monkeypatch.setattr(updater, \"Urllib3Fetcher\", lambda app_user_agent: MockFetcher())\n\n    # Using the staging TUF assets is a nice way to test but staging tuf assets expire in\n    # 3 days so faking now() becomes necessary. This correctly affects checks in\n    # _internal/trust.py as well\n    class mydatetime(datetime.datetime):\n        @classmethod\n        def now(cls, tz=None):\n            return datetime.datetime(2025, 5, 6, 0, 0, 0, 0, datetime.timezone.utc)\n\n    monkeypatch.setattr(datetime, \"datetime\", mydatetime)\n\n    return success, failure\n\n\n@pytest.fixture\ndef tuf_dirs(monkeypatch, tmp_path):\n    # Patch _get_dirs as well, to avoid polluting the user's actual cache\n    # with test assets.\n    data_dir = tmp_path / \"data\" / \"tuf\"\n    cache_dir = tmp_path / \"cache\" / \"tuf\"\n    monkeypatch.setattr(tuf, \"_get_dirs\", lambda u: (data_dir, cache_dir))\n\n    return (data_dir, cache_dir)\n\n\n@pytest.fixture\ndef sign_ctx_and_ident_for_env(\n    pytestconfig,\n    env: str,\n) -> tuple[type[SigningContext], type[IdentityToken]]:\n    \"\"\"\n    Returns a SigningContext and IdentityToken for the given environment.\n    The SigningContext is behind a callable so that it may be lazily evaluated.\n    \"\"\"\n    if env == \"staging\":\n\n        def ctx_cls():\n            return SigningContext.from_trust_config(ClientTrustConfig.staging())\n\n    elif env == \"production\":\n\n        def ctx_cls():\n            return SigningContext.from_trust_config(ClientTrustConfig.production())\n\n    else:\n        raise ValueError(f\"Unknown env {env}\")\n\n    token = os.getenv(f\"SIGSTORE_IDENTITY_TOKEN_{env}\")\n    if not token:\n        # If the variable is not defined, try getting an ambient token.\n        token = detect_credential(TEST_CLIENT_ID)\n\n    return ctx_cls, IdentityToken(token)\n\n\n@pytest.fixture\ndef staging() -> tuple[type[SigningContext], type[Verifier], IdentityToken]:\n    \"\"\"\n    Returns a SigningContext, Verifier, and IdentityToken for the staging environment.\n    The SigningContext and Verifier are both behind callables so that they may be lazily evaluated.\n    \"\"\"\n\n    def signer():\n        return SigningContext.from_trust_config(ClientTrustConfig.staging())\n\n    verifier = Verifier.staging\n\n    # Detect env variable for local interactive tests.\n    token = os.getenv(\"SIGSTORE_IDENTITY_TOKEN_staging\")\n    if not token:\n        # If the variable is not defined, try getting an ambient token.\n        token = detect_credential(TEST_CLIENT_ID)\n\n    return signer, verifier, IdentityToken(token)\n\n\n@pytest.fixture\ndef dummy_jwt():\n    def _dummy_jwt(claims: dict):\n        return jwt.encode(claims, key=\"definitely not secure\")\n\n    return _dummy_jwt\n\n\n@pytest.fixture\ndef tsa_url():\n    \"\"\"Return the URL of the TSA\"\"\"\n    return os.getenv(\"TEST_SIGSTORE_TIMESTAMP_AUTHORITY_URL\")\n"
  },
  {
    "path": "test/unit/internal/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n"
  },
  {
    "path": "test/unit/internal/fulcio/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n"
  },
  {
    "path": "test/unit/internal/fulcio/test_client.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n"
  },
  {
    "path": "test/unit/internal/oidc/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n"
  },
  {
    "path": "test/unit/internal/oidc/test_issuer.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom sigstore.oidc import IdentityError, Issuer, IssuerError\n\n\n@pytest.mark.online\ndef test_fail_init_url():\n    with pytest.raises(IssuerError):\n        Issuer(\"https://google.com\")\n\n\n@pytest.mark.online\ndef test_init_url():\n    Issuer(\"https://accounts.google.com\")\n\n\n@pytest.mark.online\ndef test_get_identity_token_bad_code(monkeypatch):\n    # Send token request to oauth2.sigstage.dev but provide a bogus authorization code\n    monkeypatch.setattr(\"builtins.input\", lambda _: \"hunter2\")\n    with pytest.raises(IdentityError, match=r\"^Token request failed with .+$\"):\n        Issuer(\"https://oauth2.sigstage.dev/auth\").identity_token(force_oob=True)\n\n\ndef test_identity_token_csrf_protection():\n    \"\"\"\n    Verify that identity_token() raises IdentityError when the returned state\n    does not match the session state (CSRF protection).\n    \"\"\"\n    with (\n        patch(\"sigstore.oidc.webbrowser.open\"),\n        patch(\"sigstore._internal.oidc.oauth._OAuthFlow\") as MockOAuthFlow,\n        patch(\"sigstore.oidc.requests.Session\") as MockSession,\n        patch(\"sigstore.oidc.IdentityToken\"),\n    ):\n        # Setup the mock server returned by _OAuthFlow context manager\n        mock_server = MagicMock()\n        MockOAuthFlow.return_value.__enter__.return_value = mock_server\n\n        # Simulate a mismatching state\n        original_state = \"original-secure-state\"\n        malicious_state = \"malicious-state\"\n\n        # The session has the original state (we mock the property access)\n        # Since we added a property 'state', we need to make sure the mock returns it.\n        # But here we are mocking the whole server object.\n        # server.oauth_session.state\n        mock_server.oauth_session.state = original_state\n\n        mock_server.is_oob.return_value = False\n        mock_server.base_uri = \"http://localhost:12345\"\n        mock_server.redirect_uri = \"http://localhost:12345/callback\"\n\n        # The auth response simulates what the redirect handler receives\n        mock_server.auth_response = {\n            \"code\": [\"fake-code\"],\n            \"state\": [malicious_state],\n        }\n\n        # Mock responses for Issuer initialization and token exchange\n        mock_session_instance = MockSession.return_value\n\n        # Mock .well-known/openid-configuration response\n        mock_config_response = MagicMock()\n        mock_config_response.json.return_value = {\n            \"authorization_endpoint\": \"https://auth.example.com\",\n            \"token_endpoint\": \"https://token.example.com\",\n        }\n        mock_config_response.raise_for_status.return_value = None\n\n        mock_session_instance.get.side_effect = [mock_config_response]\n\n        # Initialize Issuer\n        issuer = Issuer(\"https://issuer.example.com\")\n\n        # Call identity_token() and expect IdentityError due to state mismatch\n        with pytest.raises(IdentityError, match=\"OAuth state mismatch\"):\n            issuer.identity_token()\n"
  },
  {
    "path": "test/unit/internal/rekor/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n"
  },
  {
    "path": "test/unit/internal/rekor/test_client_v2.py",
    "content": "# Copyright 2025 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport hashlib\n\nimport pytest\n\nfrom sigstore import dsse\nfrom sigstore.models import TransparencyLogEntry\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_rekor_v2_create_entry_dsse(staging):\n    # This is not a real unit test: it requires not only staging rekor but also TUF\n    # fulcio and oidc -- maybe useful only until we have real integration tests in place\n    sign_ctx_cls, _, identity = staging\n    sign_ctx = sign_ctx_cls()\n\n    stmt = (\n        dsse.StatementBuilder()\n        .subjects(\n            [\n                dsse.Subject(\n                    name=\"null\", digest={\"sha256\": hashlib.sha256(b\"\").hexdigest()}\n                )\n            ]\n        )\n        .predicate_type(\"https://cosign.sigstore.dev/attestation/v1\")\n        .predicate(\n            {\n                \"Data\": \"\",\n                \"Timestamp\": \"2023-12-07T00:37:58Z\",\n            }\n        )\n    ).build()\n\n    with sign_ctx.signer(identity) as signer:\n        bundle = signer.sign_dsse(stmt)\n\n    assert isinstance(bundle.log_entry, TransparencyLogEntry)\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_rekor_v2_create_entry_hashed_rekord(staging):\n    # This is not a real unit test: it requires not only staging rekor but also TUF\n    # fulcio and oidc -- maybe useful only until we have real integration tests in place\n    sign_ctx_cls, _, identity = staging\n    sign_ctx = sign_ctx_cls()\n\n    with sign_ctx.signer(identity) as signer:\n        bundle = signer.sign_artifact(b\"\")\n\n    assert isinstance(bundle.log_entry, TransparencyLogEntry)\n"
  },
  {
    "path": "test/unit/internal/test_key_details.py",
    "content": "# Copyright 2025 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom unittest.mock import Mock\n\nimport pytest\nfrom cryptography.exceptions import UnsupportedAlgorithm\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import dsa, ec, ed25519, padding, rsa\nfrom sigstore_models.common.v1 import PublicKeyDetails\n\nfrom sigstore._internal.key_details import _get_key_details\n\n\n# The algorithms tested below are from https://github.com/sigstore/fulcio/blob/4a86d8bf45972b58051ba44d91cd96664cf74711/cmd/app/serve.go#L125-L133\n@pytest.mark.parametrize(\n    \"mock_certificate\",\n    [\n        # ec\n        Mock(\n            public_key=Mock(\n                return_value=ec.generate_private_key(ec.SECP256R1()).public_key()\n            )\n        ),\n        Mock(\n            public_key=Mock(\n                return_value=ec.generate_private_key(ec.SECP384R1()).public_key()\n            )\n        ),\n        Mock(\n            public_key=Mock(\n                return_value=ec.generate_private_key(ec.SECP521R1()).public_key()\n            )\n        ),\n        # rsa pkcs1\n        Mock(\n            public_key=Mock(\n                return_value=rsa.generate_private_key(\n                    public_exponent=65537, key_size=2048\n                ).public_key()\n            ),\n            signature_algorithm_parameters=padding.PKCS1v15(),\n        ),\n        Mock(\n            public_key=Mock(\n                return_value=rsa.generate_private_key(\n                    public_exponent=65537, key_size=3072\n                ).public_key()\n            ),\n            signature_algorithm_parameters=padding.PKCS1v15(),\n        ),\n        Mock(\n            public_key=Mock(\n                return_value=rsa.generate_private_key(\n                    public_exponent=65537, key_size=4096\n                ).public_key()\n            ),\n            signature_algorithm_parameters=padding.PKCS1v15(),\n        ),\n        # ed25519\n        Mock(\n            public_key=Mock(\n                return_value=ed25519.Ed25519PrivateKey.generate().public_key(),\n                signature_algorithm_parameters=None,\n            )\n        ),\n    ],\n)\ndef test_get_key_details(mock_certificate):\n    \"\"\"\n    Ensures that we return a PublicKeyDetails for supported key types and schemes.\n    \"\"\"\n    key_details = _get_key_details(mock_certificate)\n    assert isinstance(key_details, PublicKeyDetails)\n\n\ndef delayed_crypto_mock(mock_func, error_msg):\n    # execute mock_func, mark test as skipped if cryptography does not support this algo.\n    # This is done so missing support does not break the negative test collection\n    try:\n        data = mock_func()\n        return pytest.param(data, error_msg)\n    except UnsupportedAlgorithm as e:\n        return pytest.param(\n            None,\n            error_msg,\n            marks=pytest.mark.skip(reason=f\"missing cryptography support: {e}\"),\n        )\n\n\nclass DummyCurve(ec.EllipticCurve):\n    name = \"dummycurve\"\n\n    @property\n    def key_size(self):\n        return 69420\n\n    @property\n    def group_order(self):\n        return 69420\n\n\n@pytest.mark.parametrize(\n    \"mock_certificate, error_msg\",\n    [\n        # Unsupported EC curve\n        delayed_crypto_mock(\n            lambda: Mock(\n                public_key=Mock(\n                    return_value=ec.generate_private_key(DummyCurve()).public_key()\n                )\n            ),\n            \"Unsupported EC curve: dummycurve\",\n        ),\n        # Unsupported RSA padding\n        delayed_crypto_mock(\n            lambda: Mock(\n                public_key=Mock(\n                    return_value=rsa.generate_private_key(\n                        public_exponent=65537, key_size=2048\n                    ).public_key()\n                ),\n                signature_algorithm_parameters=padding.PSS(\n                    mgf=padding.MGF1(hashes.SHA256()),\n                    salt_length=padding.PSS.MAX_LENGTH,\n                ),\n            ),\n            \"Unsupported public key type, size, and padding\",\n        ),\n        delayed_crypto_mock(\n            lambda: Mock(\n                public_key=Mock(\n                    return_value=rsa.generate_private_key(\n                        public_exponent=65537, key_size=3072\n                    ).public_key()\n                ),\n                signature_algorithm_parameters=padding.PSS(\n                    mgf=padding.MGF1(hashes.SHA256()),\n                    salt_length=padding.PSS.MAX_LENGTH,\n                ),\n            ),\n            \"Unsupported public key type, size, and padding\",\n        ),\n        delayed_crypto_mock(\n            lambda: Mock(\n                public_key=Mock(\n                    return_value=rsa.generate_private_key(\n                        public_exponent=65537, key_size=4096\n                    ).public_key()\n                ),\n                signature_algorithm_parameters=padding.PSS(\n                    mgf=padding.MGF1(hashes.SHA256()),\n                    salt_length=padding.PSS.MAX_LENGTH,\n                ),\n            ),\n            \"Unsupported public key type, size, and padding\",\n        ),\n        # Unsupported RSA key size\n        delayed_crypto_mock(\n            lambda: Mock(\n                public_key=Mock(\n                    return_value=rsa.generate_private_key(\n                        public_exponent=65537, key_size=1024\n                    ).public_key()\n                ),\n                signature_algorithm_parameters=padding.PKCS1v15(),\n            ),\n            \"Unsupported RSA key size: 1024\",\n        ),\n        # Unsupported key type\n        delayed_crypto_mock(\n            lambda: Mock(\n                public_key=Mock(\n                    return_value=dsa.generate_private_key(key_size=1024).public_key()\n                )\n            ),\n            \"Unsupported public key type\",\n        ),\n    ],\n)\ndef test_get_key_details_unsupported(mock_certificate, error_msg):\n    \"\"\"\n    Ensures that we raise a ValueError for unsupported key types and schemes.\n    \"\"\"\n    with pytest.raises(ValueError, match=error_msg):\n        _get_key_details(mock_certificate)\n"
  },
  {
    "path": "test/unit/internal/test_sct.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport datetime\nimport struct\n\nimport pretend\nimport pytest\nfrom cryptography.x509.certificate_transparency import LogEntryType\n\nfrom sigstore._internal import sct\n\n\n@pytest.mark.parametrize(\n    \"precert_bytes_len\",\n    [\n        3,\n        255,\n        1024,\n        16777215,\n    ],\n)\ndef test_pack_digitally_signed_precertificate(precert_bytes_len):\n    precert_bytes = b\"x\" * precert_bytes_len\n\n    mock_sct = pretend.stub(\n        version=pretend.stub(value=0),\n        timestamp=datetime.datetime.fromtimestamp(\n            1234 / 1000.0, tz=datetime.timezone.utc\n        ),\n        entry_type=LogEntryType.PRE_CERTIFICATE,\n        extension_bytes=b\"\",\n    )\n    cert = pretend.stub(tbs_precertificate_bytes=precert_bytes)\n    issuer_key_hash = b\"iamapublickeyshatwofivesixdigest\"\n\n    _, l1, l2, l3 = struct.unpack(\"!4c\", struct.pack(\"!I\", len(precert_bytes)))\n\n    data = sct._pack_digitally_signed(mock_sct, cert, issuer_key_hash)\n    assert data == (\n        b\"\\x00\"  # version\n        b\"\\x00\"  # signature type\n        b\"\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\xd2\"  # timestamp\n        b\"\\x00\\x01\"  # entry type\n        b\"iamapublickeyshatwofivesixdigest\"  # issuer key hash\n        + l1\n        + l2\n        + l3  # tbs cert length\n        + precert_bytes  # tbs cert\n        + b\"\\x00\\x00\"  # extensions length\n        + b\"\"  # extensions\n    )\n"
  },
  {
    "path": "test/unit/internal/test_timestamping.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport pytest\nimport requests\n\nfrom sigstore._internal.timestamp import TimestampAuthorityClient, TimestampError\nfrom sigstore._utils import sha256_digest\n\n\n@pytest.mark.timestamp_authority\nclass TestTimestampAuthorityClient:\n    def test_sign_request(self, tsa_url: str):\n        tsa = TimestampAuthorityClient(tsa_url)\n        response = tsa.request_timestamp(b\"hello\")\n        assert response\n        assert (\n            response.tst_info.message_imprint.message == sha256_digest(b\"hello\").digest\n        )\n        assert (\n            response.tst_info.message_imprint.hash_algorithm.dotted_string\n            == \"2.16.840.1.101.3.4.2.1\"\n        )  # SHA256 OID\n\n    def test_sign_request_invalid_url(self):\n        tsa = TimestampAuthorityClient(\"http://fake-url\")\n        with pytest.raises(TimestampError, match=\"error while sending\"):\n            tsa.request_timestamp(b\"hello\")\n\n    def test_sign_request_invalid_request(self, tsa_url):\n        tsa = TimestampAuthorityClient(tsa_url)\n        with pytest.raises(TimestampError, match=\"invalid request\"):\n            tsa.request_timestamp(b\"\")  # empty value here\n\n    def test_invalid_response(self, tsa_url, monkeypatch):\n        monkeypatch.setattr(requests.Response, \"content\", b\"invalid-response\")\n\n        tsa = TimestampAuthorityClient(tsa_url)\n        with pytest.raises(TimestampError, match=\"invalid response\"):\n            tsa.request_timestamp(b\"hello\")\n"
  },
  {
    "path": "test/unit/internal/test_trust.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\nimport os\nfrom datetime import datetime, timedelta, timezone\n\nimport pytest\nfrom sigstore_models.common.v1 import TimeRange\nfrom sigstore_models.trustroot.v1 import (\n    Service,\n    ServiceConfiguration,\n    ServiceSelector,\n)\n\nfrom sigstore._internal.fulcio.client import FulcioClient\nfrom sigstore._internal.rekor.client import RekorClient\nfrom sigstore._internal.rekor.client_v2 import RekorV2Client\nfrom sigstore._internal.timestamp import TimestampAuthorityClient\nfrom sigstore._internal.trust import (\n    CertificateAuthority,\n    KeyringPurpose,\n)\nfrom sigstore._utils import is_timerange_valid\nfrom sigstore.errors import Error, TUFError\nfrom sigstore.models import (\n    ClientTrustConfig,\n    SigningConfig,\n    TrustedRoot,\n)\n\n# Test data for TestSigningcconfig\n_service_v1_op1 = Service(url=\"url1\", major_api_version=1, operator=\"op1\")\n_service2_v1_op1 = Service(url=\"url2\", major_api_version=1, operator=\"op1\")\n_service_v2_op1 = Service(url=\"url3\", major_api_version=2, operator=\"op1\")\n_service_v1_op2 = Service(url=\"url4\", major_api_version=1, operator=\"op2\")\n_service_v1_op3 = Service(url=\"url5\", major_api_version=1, operator=\"op3\")\n_service_v1_op4 = Service(\n    url=\"url6\",\n    major_api_version=1,\n    operator=\"op4\",\n    valid_for=TimeRange(start=datetime(3000, 1, 1, tzinfo=timezone.utc)),\n)\n\n\nclass TestCertificateAuthority:\n    def test_good(self, asset):\n        path = asset(\"trusted_root/certificate_authority.json\")\n        authority = CertificateAuthority.from_json(path)\n\n        assert len(authority.certificates(allow_expired=True)) == 3\n        assert authority.validity_period_end is not None\n        assert authority.validity_period_start < authority.validity_period_end\n\n    def test_missing_root(self, asset):\n        path = asset(\"trusted_root/certificate_authority.empty.json\")\n        with pytest.raises(Error, match=\"missing a certificate\"):\n            CertificateAuthority.from_json(path)\n\n\nclass TestSigningConfig:\n    def test_good(self, asset):\n        path = asset(\"signing_config/signingconfig.v2.json\")\n        signing_config = SigningConfig.from_file(path)\n\n        assert (\n            signing_config._inner.media_type\n            == SigningConfig.SigningConfigType.SIGNING_CONFIG_0_2.value\n        )\n\n        fulcio = signing_config.get_fulcio()\n        assert isinstance(fulcio, FulcioClient)\n        assert fulcio.url == \"https://fulcio.example.com\"\n        assert signing_config.get_oidc_url() == \"https://oauth2.example.com/auth\"\n\n        # signing config contains v1 and v2, we pick v2\n        tlogs = signing_config.get_tlogs()\n        assert len(tlogs) == 1\n        assert isinstance(tlogs[0], RekorV2Client)\n        assert tlogs[0].url == \"https://rekor-v2.example.com/api/v2\"\n\n        tsas = signing_config.get_tsas()\n        assert len(tsas) == 1\n        assert isinstance(tsas[0], TimestampAuthorityClient)\n        assert tsas[0].url == \"https://timestamp.example.com/api/v1/timestamp\"\n\n    def test_good_only_v1_rekor(self, asset):\n        \"\"\"Test case where a rekor 2 instance is not available\"\"\"\n        path = asset(\"signing_config/signingconfig-only-v1-rekor.v2.json\")\n        signing_config = SigningConfig.from_file(path)\n\n        tlogs = signing_config.get_tlogs()\n        assert len(tlogs) == 1\n        assert isinstance(tlogs[0], RekorClient)\n        assert tlogs[0].url == \"https://rekor.example.com/api/v1\"\n\n    @pytest.mark.parametrize(\n        \"services, versions, config, expected_result\",\n        [\n            pytest.param(\n                [_service_v1_op1],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.ALL),\n                [_service_v1_op1],\n                id=\"base case\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service2_v1_op1],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.ALL),\n                [_service2_v1_op1],\n                id=\"multiple services, same operator: expect 1 service in result\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v1_op2],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.ALL),\n                [_service_v1_op1, _service_v1_op2],\n                id=\"2 services, different operator: expect 2 services in result\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v1_op2, _service_v1_op4],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.ALL),\n                [_service_v1_op1, _service_v1_op2],\n                id=\"3 services, one is not yet valid: expect 2 services in result\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v1_op2],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.ANY),\n                [_service_v1_op1],\n                id=\"ANY selector: expect 1 service only in result\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v1_op2, _service_v1_op3],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.EXACT, count=2),\n                [_service_v1_op1, _service_v1_op2],\n                id=\"EXACT selector: expect configured number of services in result\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v2_op1],\n                [1, 2],\n                ServiceConfiguration(selector=ServiceSelector.ALL),\n                [_service_v2_op1],\n                id=\"services with different version: expect highest version\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v2_op1],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.ALL),\n                [_service_v1_op1],\n                id=\"services with different version: expect the supported version\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v1_op2],\n                [2],\n                ServiceConfiguration(selector=ServiceSelector.ALL),\n                [],\n                id=\"No supported versions: expect no results\",\n            ),\n            pytest.param(\n                [_service_v1_op1, _service_v2_op1, _service_v1_op2],\n                [1],\n                None,\n                [_service_v1_op1, _service_v1_op2],\n                id=\"services without ServiceConfiguration: expect all supported\",\n            ),\n        ],\n    )\n    def test_get_valid_services(self, services, versions, config, expected_result):\n        result = SigningConfig._get_valid_services(services, versions, config)\n\n        assert result == expected_result\n\n    @pytest.mark.parametrize(\n        \"services, versions, config\",\n        [\n            (  # EXACT selector without enough services\n                [_service_v1_op1],\n                [1],\n                ServiceConfiguration(selector=ServiceSelector.EXACT, count=2),\n            ),\n        ],\n    )\n    def test_get_valid_services_fail(self, services, versions, config):\n        with pytest.raises(ValueError):\n            SigningConfig._get_valid_services(services, versions, config)\n\n\nclass TestTrustedRoot:\n    @pytest.mark.parametrize(\n        \"file\",\n        [\n            \"trusted_root/trustedroot.v1.json\",\n            \"trusted_root/trustedroot.v1.local_tlog_ed25519_rekor-tiles.json\",\n        ],\n    )\n    def test_good(self, asset, file):\n        \"\"\"\n        Ensures that the trusted_roots are well-formed and that the expected embedded keys are supported.\n        \"\"\"\n        path = asset(file)\n        root = TrustedRoot.from_file(path)\n\n        assert (\n            root._inner.media_type == TrustedRoot.TrustedRootType.TRUSTED_ROOT_0_1.value\n        )\n        assert len(root._inner.tlogs) == 1\n        assert len(root._inner.certificate_authorities) == 2\n        assert len(root._inner.ctlogs) == 2\n        assert len(root._inner.timestamp_authorities) == 1\n\n        # only one of the two rekor keys is actually supported\n        assert len(root.rekor_keyring(KeyringPurpose.VERIFY)._keyring) == 1\n        assert len(root.ct_keyring(KeyringPurpose.VERIFY)._keyring) == 2\n        assert root.get_fulcio_certs() is not None\n        assert root.get_timestamp_authorities() is not None\n\n    def test_bad_media_type(self, asset):\n        path = asset(\"trusted_root/trustedroot.badtype.json\")\n\n        with pytest.raises(\n            ValueError,\n            match=r\"Input should be 'application/vnd\\.dev\\.sigstore\\.trustedroot\\+json;version=0\\.1' or 'application/vnd\\.dev\\.sigstore\\.trustedroot\\.v0\\.2\\+json'\",\n        ):\n            TrustedRoot.from_file(path)\n\n\n# TODO(ww): Move these into appropriate class-scoped tests.\n\n\ndef test_trust_root_tuf_offline(mock_staging_tuf, tuf_dirs):\n    # start with empty target cache, empty local metadata dir\n    data_dir, cache_dir = tuf_dirs\n\n    # keep track of requests the TrustUpdater invoked by TrustedRoot makes\n    reqs, fail_reqs = mock_staging_tuf\n\n    trust_config = ClientTrustConfig.staging(offline=True)\n\n    # local TUF metadata is not initialized, nothing is downloaded\n    assert not os.path.exists(data_dir)\n    assert reqs == {}\n    assert fail_reqs == {}\n\n    trust_config.trusted_root.ct_keyring(purpose=KeyringPurpose.VERIFY)\n    trust_config.trusted_root.rekor_keyring(purpose=KeyringPurpose.VERIFY)\n\n    # Still no requests\n    assert reqs == {}\n    assert fail_reqs == {}\n\n\ndef test_is_timerange_valid():\n    def range_from(offset_lower=0, offset_upper=0):\n        base = datetime.now(timezone.utc)\n        return TimeRange(\n            start=base + timedelta(minutes=offset_lower),\n            end=base + timedelta(minutes=offset_upper),\n        )\n\n    # Test None should always be valid\n    assert is_timerange_valid(None, allow_expired=False)\n    assert is_timerange_valid(None, allow_expired=True)\n\n    # Test lower bound conditions\n    assert is_timerange_valid(\n        range_from(-1, 1), allow_expired=False\n    )  # Valid: 1 ago, 1 from now\n    assert not is_timerange_valid(\n        range_from(1, 1), allow_expired=False\n    )  # Invalid: 1 from now, 1 from now\n\n    # Test upper bound conditions\n    assert not is_timerange_valid(\n        range_from(-1, -1), allow_expired=False\n    )  # Invalid: 1 ago, 1 ago\n    assert is_timerange_valid(\n        range_from(-1, -1), allow_expired=True\n    )  # Valid: 1 ago, 1 ago\n\n\ndef test_trust_root_tuf_instance_error():\n    # embedded root.json is not found and no local metadata is found\n    with pytest.raises(TUFError):\n        ClientTrustConfig.from_tuf(\"foo.bar\")\n\n\ndef test_trust_root_tuf_ctfe_keys_error(monkeypatch):\n    trust_root = ClientTrustConfig.staging(offline=True).trusted_root\n    monkeypatch.setattr(trust_root._inner, \"ctlogs\", [])\n    with pytest.raises(Exception, match=\"CTFE keys not found in trusted root\"):\n        trust_root.ct_keyring(purpose=KeyringPurpose.VERIFY)\n\n\ndef test_trust_root_fulcio_certs_error(tuf_asset, monkeypatch):\n    trust_root = ClientTrustConfig.staging(offline=True).trusted_root\n    monkeypatch.setattr(trust_root._inner, \"certificate_authorities\", [])\n    with pytest.raises(\n        Exception, match=\"Fulcio certificates not found in trusted root\"\n    ):\n        trust_root.get_fulcio_certs()\n\n\nclass TestClientTrustConfig:\n    def test_good(self, asset):\n        path = asset(\"trust_config/config.v1.json\")\n        config = ClientTrustConfig.from_json(path.read_text())\n\n        assert isinstance(config.signing_config, SigningConfig)\n        assert isinstance(config.trusted_root, TrustedRoot)\n\n    def test_bad_media_type(self, asset):\n        path = asset(\"trust_config/config.badtype.json\")\n\n        with pytest.raises(\n            ValueError,\n            match=r\"Input should be 'application/vnd\\.dev\\.sigstore\\.clienttrustconfig.v0.1\\+json'\",\n        ):\n            ClientTrustConfig.from_json(path.read_text())\n"
  },
  {
    "path": "test/unit/test_dsse.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport base64\nimport json\n\nimport pytest\n\nfrom sigstore import dsse\nfrom sigstore.dsse import InvalidEnvelope\n\n\nclass TestEnvelope:\n    def test_roundtrip(self):\n        raw = json.dumps(\n            {\n                \"payload\": base64.b64encode(b\"foo\").decode(),\n                \"payloadType\": dsse.Envelope._TYPE,\n                \"signatures\": [\n                    {\"sig\": base64.b64encode(b\"lol\").decode()},\n                ],\n            }\n        )\n        evp = dsse.Envelope._from_json(raw)\n\n        assert evp._inner.payload == b\"foo\"\n        assert evp._inner.payload_type == dsse.Envelope._TYPE\n        assert evp.signature == b\"lol\"\n\n        serialized = evp.to_json()\n        # envelope matches\n        assert dsse.Envelope._from_json(serialized) == evp\n        # parsed JSON marches\n        assert json.loads(raw) == evp._inner.to_dict()\n\n    def test_missing_signature(self):\n        raw = json.dumps(\n            {\n                \"payload\": base64.b64encode(b\"foo\").decode(),\n                \"payloadType\": dsse.Envelope._TYPE,\n                \"signatures\": [],\n            }\n        )\n\n        with pytest.raises(InvalidEnvelope, match=\"one signature\"):\n            dsse.Envelope._from_json(raw)\n\n    def test_empty_signature(self):\n        raw = json.dumps(\n            {\n                \"payload\": base64.b64encode(b\"foo\").decode(),\n                \"payloadType\": dsse.Envelope._TYPE,\n                \"signatures\": [\n                    {\"sig\": \"\"},\n                ],\n            }\n        )\n\n        with pytest.raises(InvalidEnvelope, match=\"non-empty\"):\n            dsse.Envelope._from_json(raw)\n\n    def test_multiple_signatures(self):\n        raw = json.dumps(\n            {\n                \"payload\": base64.b64encode(b\"foo\").decode(),\n                \"payloadType\": dsse.Envelope._TYPE,\n                \"signatures\": [\n                    {\"sig\": base64.b64encode(b\"lol\").decode()},\n                    {\"sig\": base64.b64encode(b\"lmao\").decode()},\n                ],\n            }\n        )\n\n        with pytest.raises(InvalidEnvelope, match=\"one signature\"):\n            dsse.Envelope._from_json(raw)\n"
  },
  {
    "path": "test/unit/test_hashes.py",
    "content": "# Copyright 2024 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport hashlib\n\nimport pytest\nfrom sigstore_models.common.v1 import HashAlgorithm\n\nfrom sigstore.hashes import Hashed\n\n\nclass TestHashes:\n    @pytest.mark.parametrize(\n        (\"algorithm\", \"digest\"),\n        [\n            (HashAlgorithm.SHA2_256, hashlib.sha256(b\"\").hexdigest()),\n            (HashAlgorithm.SHA2_384, hashlib.sha384(b\"\").hexdigest()),\n            (HashAlgorithm.SHA2_512, hashlib.sha512(b\"\").hexdigest()),\n            (HashAlgorithm.SHA3_256, hashlib.sha3_256(b\"\").hexdigest()),\n            (HashAlgorithm.SHA3_384, hashlib.sha3_384(b\"\").hexdigest()),\n        ],\n    )\n    def test_hashed_repr(self, algorithm, digest):\n        hashed = Hashed(algorithm=algorithm, digest=bytes.fromhex(digest))\n        assert str(hashed) == f\"{algorithm.name}:{digest}\"\n"
  },
  {
    "path": "test/unit/test_models.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport json\nfrom base64 import b64encode\n\nimport pytest\nfrom sigstore_models.rekor.v1 import KindVersion\nfrom sigstore_models.rekor.v1 import TransparencyLogEntry as _TransparencyLogEntry\n\nfrom sigstore.errors import VerificationError\nfrom sigstore.models import (\n    Bundle,\n    InvalidBundle,\n    TimestampVerificationData,\n    TransparencyLogEntry,\n    VerificationMaterial,\n)\n\n\nclass TestTransparencyLogEntry:\n    @pytest.mark.parametrize(\"integrated_time\", [0, 1746819403])\n    def test_missing_inclusion_proof(self, integrated_time: int):\n        with pytest.raises(ValueError, match=r\"inclusion_proof\"):\n            TransparencyLogEntry(\n                _TransparencyLogEntry(\n                    kind_version=KindVersion(kind=\"hashedrekord\", version=\"fake\"),\n                    canonicalized_body=b64encode(b\"fake\"),\n                    integrated_time=integrated_time,\n                    log_id=\"1234\",\n                    log_index=1,\n                    inclusion_proof=None,\n                    inclusion_promise=None,\n                )\n            )\n\n    # def test_missing_inclusion_promise_and_integrated_time_round_trip(\n    #     self, signing_bundle\n    # ):\n    #     \"\"\"\n    #     Ensures that LogEntry._to_rekor() succeeds even without an inclusion_promise and integrated_time.\n    #     \"\"\"\n    #     bundle: Bundle\n    #     _, bundle = signing_bundle(\"bundle.txt\")\n    #     _dict = bundle.log_entry._to_rekor().to_dict()\n    #     print(_dict)\n    #     del _dict[\"inclusionPromise\"]\n    #     del _dict[\"integratedTime\"]\n    #     entry = LogEntry._from_dict_rekor(_dict)\n    #     assert entry.inclusion_promise is None\n    #     assert entry._to_rekor() is not None\n    #     assert LogEntry._from_dict_rekor(entry._to_rekor().to_dict()) == entry\n\n    def test_logentry_roundtrip(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle.txt\")\n\n        assert (\n            TransparencyLogEntry(\n                _TransparencyLogEntry.from_dict(bundle.log_entry._inner.to_dict())\n            )\n            == bundle.log_entry\n        )\n\n\nclass TestTimestampVerificationData:\n    \"\"\"\n    Tests for the `TimestampVerificationData` wrapper model.\n    \"\"\"\n\n    def test_valid_timestamp(self, asset):\n        timestamp = {\n            \"rfc3161Timestamps\": [\n                {\n                    \"signedTimestamp\": \"MIIEgTADAgEAMIIEeAYJKoZIhvcNAQcCoIIEaTCCBGUCAQMxDTALBglghkgBZQMEAgEwgc8GCyqGSIb3DQEJEAEEoIG/BIG8MIG5AgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgyGobd7rprYIL0JTus5EpEb7jrrecS+cMbb42ftjtm+UCFBV/kwOOwt0tdtYXK1FGhXf7W4oFGA8yMDI0MTAyMjA3MzEwNVowAwIBAQIUTo190a2ixXglxLh7KJcwj6B4kf+gNKQyMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmegggHRMIIBzTCCAXKgAwIBAgIUIYzlmDAtGrQ5jmcZpeAN0Wyj8Q8wCgYIKoZIzj0EAwIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZTAeFw0yNDEwMjIwNzIyNTNaFw0zMzEwMjIwNzI1NTNaMDAxDjAMBgNVBAoTBWxvY2FsMR4wHAYDVQQDExVUZXN0IFRTQSBUaW1lc3RhbXBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQBhKWvDUj1+VFrWudnWIRzAug99WAydJuyF9pxneWppyXbjio3RSoNBvhg+91eeue7GpRQx5ZoxdeiHJD5p7Z0o2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFD7JreyIuE9lHC9k+cFePRXIPdNaMB8GA1UdIwQYMBaAFJMEP2b7r8olhCtvCokuFyTMC0nOMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0kAMEYCIQC69iKNrM4N2/OHksX7zEJM7ImGR+Puq7ALM8l3+riChgIhAKbEWTmifAE6VaQwnL0NNTJskSgk6r8BzvbJtJEZpk6fMYIBqDCCAaQCAQEwSDAwMQ4wDAYDVQQKEwVsb2NhbDEeMBwGA1UEAxMVVGVzdCBUU0EgSW50ZXJtZWRpYXRlAhQhjOWYMC0atDmOZxml4A3RbKPxDzALBglghkgBZQMEAgGggfMwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMjIwNzMxMDVaMC8GCSqGSIb3DQEJBDEiBCBr9fx6gIRsipdGxMDIw1tpvHUv3y10SHUzEM+HHP15+DCBhQYLKoZIhvcNAQkQAi8xdjB0MHIwcAQg2PR1japGgjWt7Cd0jQJrSYlYTblz/UeoJw0LkbqIsSIwTDA0pDIwMDEOMAwGA1UEChMFbG9jYWwxHjAcBgNVBAMTFVRlc3QgVFNBIEludGVybWVkaWF0ZQIUIYzlmDAtGrQ5jmcZpeAN0Wyj8Q8wCgYIKoZIzj0EAwIERjBEAiBDfeCcnA1qIlHfMK/u3FZ1HtS9840NnXXaRdMD4R7MywIgZfoBiAMV3SFqO71+eo2kD9oBkW49Pb9eoQs00nOlvn8=\"\n                }\n            ]\n        }\n\n        timestamp_verification = TimestampVerificationData.from_json(\n            json.dumps(timestamp)\n        )\n\n        assert timestamp_verification.rfc3161_timestamps\n\n    def test_no_timestamp(self, asset):\n        timestamp = {\"rfc3161Timestamps\": []}\n        timestamp_verification = TimestampVerificationData.from_json(\n            json.dumps(timestamp)\n        )\n\n        assert not timestamp_verification.rfc3161_timestamps\n\n    def test_invalid_timestamp(self, asset):\n        timestamp = {\"rfc3161Timestamps\": [{\"signedTimestamp\": \"invalid-entry\"}]}\n        with pytest.raises(VerificationError, match=\"Invalid Timestamp\"):\n            TimestampVerificationData.from_json(json.dumps(timestamp))\n\n\nclass TestVerificationMaterial:\n    \"\"\"\n    Tests for the `VerificationMaterial` wrapper model.\n    \"\"\"\n\n    def test_valid_verification_material(self, asset):\n        bundle = Bundle.from_json(asset(\"bundle.txt.sigstore\").read_bytes())\n\n        verification_material = VerificationMaterial(\n            bundle._inner.verification_material\n        )\n        assert verification_material\n\n\nclass TestBundle:\n    \"\"\"\n    Tests for the `Bundle` wrapper model.\n    \"\"\"\n\n    def test_invalid_bundle_version(self, signing_bundle):\n        with pytest.raises(InvalidBundle, match=\"failed to load bundle\"):\n            signing_bundle(\"bundle_invalid_version.txt\")\n\n    def test_invalid_empty_cert_chain(self, signing_bundle):\n        with pytest.raises(\n            InvalidBundle, match=\"expected non-empty certificate chain in bundle\"\n        ):\n            signing_bundle(\"bundle_no_cert_v1.txt\")\n\n    def test_invalid_no_log_entry(self, signing_bundle):\n        with pytest.raises(\n            InvalidBundle, match=\"expected exactly one log entry in bundle\"\n        ):\n            signing_bundle(\"bundle_no_log_entry.txt\")\n\n    def test_verification_materials_offline_no_checkpoint(self, signing_bundle):\n        with pytest.raises(\n            InvalidBundle, match=\"entry must contain inclusion proof, with checkpoint\"\n        ):\n            signing_bundle(\"bundle_no_checkpoint.txt\")\n\n    def test_bundle_roundtrip(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle.txt\")\n\n        # Bundles are not directly comparable, but a round-trip preserves their\n        # underlying object structure.\n        assert json.loads(Bundle.from_json(bundle.to_json()).to_json()) == json.loads(\n            bundle.to_json()\n        )\n\n    def test_bundle_missing_signed_time(self, signing_bundle):\n        with pytest.raises(\n            InvalidBundle,\n            match=r\"bundle must contain an inclusion promise or signed timestamp\\(s\\)\",\n        ):\n            signing_bundle(\"bundle_v3_no_signed_time.txt\")\n\n\nclass TestKnownBundleTypes:\n    def test_str(self):\n        for type_ in Bundle.BundleType:\n            assert str(type_) == type_.value\n            assert type_ in Bundle.BundleType\n"
  },
  {
    "path": "test/unit/test_oidc.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport datetime\n\nimport pytest\n\nfrom sigstore import oidc\n\n\nclass TestIdentityToken:\n    def test_invalid_jwt(self):\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(\"invalid jwt\")\n\n    def test_missing_iss(self, dummy_jwt):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now,\n                \"exp\": now + 600,\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(jwt)\n\n    def test_missing_aud(self, dummy_jwt):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now,\n                \"exp\": now + 600,\n                \"iss\": \"fake-issuer\",\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(jwt)\n\n    @pytest.mark.parametrize(\"aud\", (None, \"not-sigstore\"))\n    def test_invalid_aud(self, dummy_jwt, aud):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": aud,\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now,\n                \"exp\": now + 600,\n                \"iss\": \"fake-issuer\",\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(jwt)\n\n    def test_missing_iat(self, dummy_jwt):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"nbf\": now,\n                \"exp\": now + 600,\n                \"iss\": \"fake-issuer\",\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(jwt)\n\n    @pytest.mark.parametrize(\"iat\", (None, \"not-an-int\"))\n    def test_invalid_iat(self, dummy_jwt, iat):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": iat,\n                \"nbf\": now,\n                \"exp\": now + 600,\n                \"iss\": \"fake-issuer\",\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(jwt)\n\n    def test_missing_nbf_ok(self, dummy_jwt):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"iat\": now,\n                \"exp\": now + 600,\n                \"iss\": \"fake-issuer\",\n                \"sub\": \"sigstore\",\n            }\n        )\n\n        assert oidc.IdentityToken(jwt) is not None\n\n    def test_invalid_nbf(self, dummy_jwt):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now + 600,\n                \"exp\": now + 601,\n                \"iss\": \"fake-issuer\",\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError,\n            match=\"Identity token is not within its validity period\",\n        ):\n            oidc.IdentityToken(jwt)\n\n    def test_missing_exp(self, dummy_jwt):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now,\n                \"iss\": \"fake-issuer\",\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(jwt)\n\n    def test_invalid_exp(self, dummy_jwt):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": now - 600,\n                \"nbf\": now - 300,\n                # NOTE: 6 seconds due to +/- 5 second flutter.\n                \"exp\": now - 6,\n                \"iss\": \"fake-issuer\",\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError, match=\"Identity token is malformed or missing claims\"\n        ):\n            oidc.IdentityToken(jwt)\n\n    @pytest.mark.parametrize(\n        \"iss\", [k for k, v in oidc._KNOWN_OIDC_ISSUERS.items() if v != \"sub\"]\n    )\n    def test_missing_identity_claim(self, dummy_jwt, iss):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now,\n                \"exp\": now + 600,\n                \"iss\": iss,\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError,\n            match=r\"Identity token is missing the required '.+' claim\",\n        ):\n            oidc.IdentityToken(jwt)\n\n    @pytest.mark.parametrize(\"fed\", (\"notadict\", {\"connector_id\": 123}))\n    def test_invalid_federated_claims(self, dummy_jwt, fed):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now,\n                \"exp\": now + 600,\n                \"iss\": \"https://accounts.google.com\",\n                \"email\": \"example@example.com\",\n                \"federated_claims\": fed,\n            }\n        )\n\n        with pytest.raises(\n            oidc.IdentityError,\n            match=\"unexpected claim type: federated_claims.*\",\n        ):\n            oidc.IdentityToken(jwt)\n\n    @pytest.mark.parametrize(\n        (\"iss\", \"identity_claim\", \"identity_value\", \"fed_iss\"),\n        [\n            (\"https://accounts.google.com\", \"email\", \"example@example.com\", None),\n            (\n                \"https://oauth2.sigstore.dev/auth\",\n                \"email\",\n                \"example@example.com\",\n                \"https://accounts.google.com\",\n            ),\n            (\"https://oauth2.sigstore.dev/auth\", \"email\", \"example@example.com\", None),\n            (\n                \"https://token.actions.githubusercontent.com\",\n                \"sub\",\n                \"some-subject\",\n                None,\n            ),\n            (\"hxxps://unknown.issuer.example.com/auth\", \"sub\", \"some-subject\", None),\n        ],\n    )\n    def test_ok(self, dummy_jwt, iss, identity_claim, identity_value, fed_iss):\n        now = int(datetime.datetime.now().timestamp())\n        jwt = dummy_jwt(\n            {\n                \"aud\": \"sigstore\",\n                \"sub\": \"fakesubject\",\n                \"iat\": now,\n                \"nbf\": now,\n                \"exp\": now + 600,\n                \"iss\": iss,\n                identity_claim: identity_value,\n                \"federated_claims\": {\"connector_id\": fed_iss},\n            }\n        )\n\n        identity = oidc.IdentityToken(jwt)\n        assert identity.in_validity_period()\n        assert identity.identity == identity_value\n        assert identity.issuer == iss\n        assert identity.federated_issuer == iss if not fed_iss else fed_iss\n"
  },
  {
    "path": "test/unit/test_session_reuse.py",
    "content": "# Copyright 2026 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport unittest.mock\n\nfrom sigstore._internal.rekor import EntryRequestBody\nfrom sigstore._internal.rekor.client import RekorClient\nfrom sigstore._internal.rekor.client_v2 import RekorV2Client\nfrom sigstore._internal.timestamp import TimestampAuthorityClient\n\n\ndef test_rekor_v1_session_reuse_public_api():\n    \"\"\"Verify that RekorClient v1 reuses its session per thread using public API.\"\"\"\n    client = RekorClient(\"http://fake\")\n\n    with unittest.mock.patch(\"requests.Session\") as mock_session_cls:\n        mock_session_inst = unittest.mock.MagicMock()\n        mock_session_cls.return_value = mock_session_inst\n\n        # Access log endpoint multiple times\n        client.log\n        client.log\n\n        # Expect 1 session\n        assert mock_session_cls.call_count == 1\n\n\ndef test_rekor_v2_session_reuse_public_api():\n    \"\"\"Verify that RekorV2Client reuses its session per thread using public API.\"\"\"\n    client = RekorV2Client(\"http://fake\")\n\n    with unittest.mock.patch(\"requests.Session\") as mock_session_cls:\n        mock_session_inst = unittest.mock.MagicMock()\n        mock_session_cls.return_value = mock_session_inst\n\n        # Call create_entry multiple times (hide exception: the client does not need to work)\n        try:\n            client.create_entry(EntryRequestBody({}))\n        except Exception:\n            pass\n\n        try:\n            client.create_entry(EntryRequestBody({}))\n        except Exception:\n            pass\n\n        # Expect 1 session\n        assert mock_session_cls.call_count == 1\n\n\ndef test_timestamp_client_session_reuse_public_api():\n    \"\"\"Verify that TimestampAuthorityClient reuses its session per thread using public API.\"\"\"\n    client = TimestampAuthorityClient(\"http://fake\")\n\n    with unittest.mock.patch(\"requests.Session\") as mock_session_cls:\n        mock_session_inst = unittest.mock.MagicMock()\n        mock_session_cls.return_value = mock_session_inst\n\n        # Call request_timestamp multiple times (hide exception: the client does not need to work)\n        try:\n            client.request_timestamp(b\"sig\")\n        except Exception:\n            pass\n\n        try:\n            client.request_timestamp(b\"sig\")\n        except Exception:\n            pass\n\n        # Expect 1 session\n        assert mock_session_cls.call_count == 1\n"
  },
  {
    "path": "test/unit/test_sign.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport hashlib\nimport secrets\n\nimport pretend\nimport pytest\nfrom sigstore_models.common.v1 import HashAlgorithm\n\nimport sigstore.oidc\nfrom sigstore.dsse import StatementBuilder, Subject\nfrom sigstore.errors import VerificationError\nfrom sigstore.hashes import Hashed\nfrom sigstore.sign import SigningContext\nfrom sigstore.verify.policy import UnsafeNoOp\n\n\n# only check the log contents for production: staging is already on\n# rekor v2 and we don't currently support log lookups on rekor v2.\n# This test can likely be removed once prod also uses rekor v2\n@pytest.mark.parametrize(\"env\", [\"production\"])\n@pytest.mark.ambient_oidc\ndef test_sign_rekor_entry_consistent(request, sign_ctx_and_ident_for_env):\n    ctx_cls, identity = sign_ctx_and_ident_for_env\n\n    # NOTE: The actual signer instance is produced lazily, so that parameter\n    # expansion doesn't fail in offline tests.\n    ctx: SigningContext = ctx_cls()\n    assert identity is not None\n\n    payload = secrets.token_bytes(32)\n    with ctx.signer(identity) as signer:\n        expected_entry = signer.sign_artifact(payload).log_entry\n\n    actual_entry = ctx._rekor.log.entries.get(log_index=expected_entry._inner.log_index)\n\n    assert (\n        expected_entry._inner.canonicalized_body\n        == actual_entry._inner.canonicalized_body\n    )\n    assert expected_entry._inner.integrated_time == actual_entry._inner.integrated_time\n    assert expected_entry._inner.log_id == actual_entry._inner.log_id\n    assert expected_entry._inner.log_index == actual_entry._inner.log_index\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_with_staging(staging, null_policy):\n    ctx_cls, verifier_cls, identity = staging\n\n    ctx: SigningContext = ctx_cls()\n    verifier = verifier_cls()\n    assert identity is not None\n\n    payload = secrets.token_bytes(32)\n    with ctx.signer(identity) as signer:\n        bundle = signer.sign_artifact(payload)\n\n    verifier.verify_artifact(payload, bundle, null_policy)\n\n\n@pytest.mark.parametrize(\"env\", [\"staging\", \"production\"])\n@pytest.mark.ambient_oidc\ndef test_sct_verify_keyring_lookup_error(sign_ctx_and_ident_for_env, monkeypatch):\n    ctx, identity = sign_ctx_and_ident_for_env\n\n    # a signer whose keyring always fails to lookup a given key.\n    ctx: SigningContext = ctx()\n    mock = pretend.stub(\n        ct_keyring=lambda *a: pretend.stub(verify=pretend.raiser(VerificationError))\n    )\n    ctx._trusted_root = mock\n    assert identity is not None\n\n    payload = secrets.token_bytes(32)\n    with pytest.raises(VerificationError, match=r\"SCT verify failed:\"):\n        with ctx.signer(identity) as signer:\n            signer.sign_artifact(payload)\n\n\n@pytest.mark.parametrize(\"env\", [\"staging\", \"production\"])\n@pytest.mark.ambient_oidc\ndef test_sct_verify_keyring_error(sign_ctx_and_ident_for_env, monkeypatch):\n    ctx, identity = sign_ctx_and_ident_for_env\n\n    # a signer whose keyring throws an internal error.\n    ctx: SigningContext = ctx()\n    mock = pretend.stub(\n        ct_keyring=lambda *a: pretend.stub(verify=pretend.raiser(VerificationError))\n    )\n    ctx._trusted_root = mock\n    assert identity is not None\n\n    payload = secrets.token_bytes(32)\n\n    with pytest.raises(VerificationError):\n        with ctx.signer(identity) as signer:\n            signer.sign_artifact(payload)\n\n\n@pytest.mark.parametrize(\"env\", [\"staging\", \"production\"])\n@pytest.mark.ambient_oidc\ndef test_identity_proof_fallback_claim(sign_ctx_and_ident_for_env, monkeypatch):\n    ctx_cls, identity = sign_ctx_and_ident_for_env\n\n    ctx: SigningContext = ctx_cls()\n    assert identity is not None\n\n    # clear out known issuers, forcing the `Identity`'s  `sub` claim to be used\n    # as fall back\n    monkeypatch.setattr(sigstore.oidc, \"_KNOWN_OIDC_ISSUERS\", {})\n\n    payload = secrets.token_bytes(32)\n\n    with ctx.signer(identity) as signer:\n        signer.sign_artifact(payload)\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_prehashed(staging):\n    sign_ctx_cls, verifier_cls, identity = staging\n\n    sign_ctx = sign_ctx_cls()\n    verifier = verifier_cls()\n\n    input_ = secrets.token_bytes(32)\n    hashed = Hashed(\n        digest=hashlib.sha256(input_).digest(), algorithm=HashAlgorithm.SHA2_256\n    )\n\n    with sign_ctx.signer(identity) as signer:\n        bundle = signer.sign_artifact(hashed)\n\n    assert bundle._inner.message_signature.message_digest.algorithm == hashed.algorithm\n    assert bundle._inner.message_signature.message_digest.digest == hashed.digest\n\n    # verifying against the original input works\n    verifier.verify_artifact(input_, bundle=bundle, policy=UnsafeNoOp())\n    # verifying against the prehash also works\n    verifier.verify_artifact(hashed, bundle=bundle, policy=UnsafeNoOp())\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_sign_dsse(staging):\n    sign_ctx, _, identity = staging\n\n    ctx = sign_ctx()\n    stmt = (\n        StatementBuilder()\n        .subjects(\n            [Subject(name=\"null\", digest={\"sha256\": hashlib.sha256(b\"\").hexdigest()})]\n        )\n        .predicate_type(\"https://cosign.sigstore.dev/attestation/v1\")\n        .predicate(\n            {\n                \"Data\": \"\",\n                \"Timestamp\": \"2023-12-07T00:37:58Z\",\n            }\n        )\n    ).build()\n\n    with ctx.signer(identity) as signer:\n        bundle = signer.sign_dsse(stmt)\n        # Ensures that all of our inner types serialize as expected.\n        bundle.to_json()\n"
  },
  {
    "path": "test/unit/test_store.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport json\n\nimport pytest\n\nfrom sigstore._utils import read_embedded\n\n\n@pytest.mark.parametrize(\n    \"env\",\n    [\n        \"https://tuf-repo-cdn.sigstore.dev\",\n        \"https://tuf-repo-cdn.sigstage.dev\",\n    ],\n)\ndef test_store_reads_root_json(env):\n    root_json = read_embedded(\"root.json\", env)\n    assert json.loads(root_json)\n\n\n@pytest.mark.parametrize(\n    \"env\",\n    [\n        \"https://tuf-repo-cdn.sigstore.dev\",\n        \"https://tuf-repo-cdn.sigstage.dev\",\n    ],\n)\ndef test_store_reads_targets_json(env):\n    trusted_root_json = read_embedded(\"trusted_root.json\", env)\n    assert json.loads(trusted_root_json)\n"
  },
  {
    "path": "test/unit/test_utils.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\nimport hashlib\nimport io\n\nimport pretend\nimport pytest\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import serialization\n\nfrom sigstore import _utils as utils\nfrom sigstore.errors import VerificationError\n\n\ndef test_key_id():\n    # Taken from certificate-transparency-go:\n    # https://github.com/google/certificate-transparency-go/blob/88227ce0/trillian/ctfe/testonly/certificates.go#L213-L231\n    precert_pem = b\"\"\"-----BEGIN CERTIFICATE-----\nMIIC3zCCAkigAwIBAgIBBzANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJHQjEk\nMCIGA1UEChMbQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5IENBMQ4wDAYDVQQIEwVX\nYWxlczEQMA4GA1UEBxMHRXJ3IFdlbjAeFw0xMjA2MDEwMDAwMDBaFw0yMjA2MDEw\nMDAwMDBaMFIxCzAJBgNVBAYTAkdCMSEwHwYDVQQKExhDZXJ0aWZpY2F0ZSBUcmFu\nc3BhcmVuY3kxDjAMBgNVBAgTBVdhbGVzMRAwDgYDVQQHEwdFcncgV2VuMIGfMA0G\nCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+75jnwmh3rjhfdTJaDB0ym+3xj6r015a/\nBH634c4VyVui+A7kWL19uG+KSyUhkaeb1wDDjpwDibRc1NyaEgqyHgy0HNDnKAWk\nEM2cW9tdSSdyba8XEPYBhzd+olsaHjnu0LiBGdwVTcaPfajjDK8VijPmyVCfSgWw\nFAn/Xdh+tQIDAQABo4HBMIG+MB0GA1UdDgQWBBQgMVQa8lwF/9hli2hDeU9ekDb3\ntDB9BgNVHSMEdjB0gBRfnYgNyHPmVNT4DdjmsMEktEfDVaFZpFcwVTELMAkGA1UE\nBhMCR0IxJDAiBgNVBAoTG0NlcnRpZmljYXRlIFRyYW5zcGFyZW5jeSBDQTEOMAwG\nA1UECBMFV2FsZXMxEDAOBgNVBAcTB0VydyBXZW6CAQAwCQYDVR0TBAIwADATBgor\nBgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0BAQUFAAOBgQACocOeAVr1Tf8CPDNg\nh1//NDdVLx8JAb3CVDFfM3K3I/sV+87MTfRxoM5NjFRlXYSHl/soHj36u0YtLGhL\nBW/qe2O0cP8WbjLURgY1s9K8bagkmyYw5x/DTwjyPdTuIo+PdPY9eGMR3QpYEUBf\nkGzKLC0+6/yBmWTr2M98CIY/vg==\n    -----END CERTIFICATE-----\"\"\"\n\n    precert = x509.load_pem_x509_certificate(precert_pem)\n\n    public_key = precert.public_key().public_bytes(\n        encoding=serialization.Encoding.DER,\n        format=serialization.PublicFormat.SubjectPublicKeyInfo,\n    )\n\n    key_id = utils.key_id(precert.public_key())\n    assert key_id == hashlib.sha256(public_key).digest()\n    assert (\n        hashlib.sha256(public_key).hexdigest()\n        == \"086c0ea25b60e3c44a994d0d5f40b81a0d44f21d63df19315e6ddfbe47373817\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"size\", [0, 1, 2, 4, 8, 32, 128, 1024, 128 * 1024, 1024 * 1024, 128 * 1024 * 1024]\n)\ndef test_sha256_streaming(size):\n    buf = b\"x\" * size\n\n    expected_digest = hashlib.sha256(buf).digest()\n    actual_digest = utils._sha256_streaming(io.BytesIO(buf))\n\n    assert expected_digest == actual_digest\n\n\ndef test_load_pem_public_key_format():\n    keybytes = b\"-----BEGIN PUBLIC KEY-----\\nbleh\\n-----END PUBLIC KEY-----\"\n    with pytest.raises(\n        VerificationError, match=\"could not load PEM-formatted public key\"\n    ):\n        utils.load_pem_public_key([keybytes])\n\n\ndef test_load_pem_public_key_serialization(monkeypatch):\n    from cryptography.hazmat.primitives import serialization\n\n    monkeypatch.setattr(serialization, \"load_pem_public_key\", lambda a: a)\n\n    keybytes = (\n        b\"-----BEGIN PUBLIC KEY-----\\n\"\n        b\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu\\n\"\n        b\"dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\\n\"\n        b\"-----END PUBLIC KEY-----\"\n    )\n\n    with pytest.raises(VerificationError, match=\"invalid key format: not one of\"):\n        utils.load_pem_public_key([keybytes])\n\n\n@pytest.mark.parametrize(\n    (\"testcase\", \"valid\"),\n    [\n        (\"bogus-root.pem\", True),\n        (\"bogus-intermediate.pem\", True),\n        (\"bogus-leaf.pem\", False),\n    ],\n)\ndef test_cert_is_ca(x509_testcase, testcase, valid):\n    cert = x509_testcase(testcase)\n\n    assert utils.cert_is_ca(cert) is valid\n\n\n@pytest.mark.parametrize(\n    \"testcase\",\n    [\n        \"bogus-root-noncritical-bc.pem\",\n        \"bogus-root-invalid-ku.pem\",\n        \"bogus-root-missing-ku.pem\",\n    ],\n)\ndef test_cert_is_ca_invalid_states(x509_testcase, testcase):\n    cert = x509_testcase(testcase)\n\n    with pytest.raises(VerificationError, match=\"invalid X.509 certificate\"):\n        utils.cert_is_ca(cert)\n\n\n@pytest.mark.parametrize(\n    (\"testcase\", \"valid\"),\n    [\n        (\"bogus-root.pem\", True),\n        (\"bogus-intermediate.pem\", False),\n        (\"bogus-leaf.pem\", False),\n        (\"bogus-leaf-invalid-ku.pem\", False),\n    ],\n)\ndef test_cert_is_root_ca(x509_testcase, testcase, valid):\n    cert = x509_testcase(testcase)\n\n    assert utils.cert_is_root_ca(cert) is valid\n\n\n@pytest.mark.parametrize(\n    (\"testcase\", \"valid\"),\n    (\n        [\"bogus-root.pem\", False],\n        [\"bogus-intermediate.pem\", False],\n        [\"bogus-intermediate-with-eku.pem\", False],\n        [\"bogus-leaf.pem\", True],\n        [\"bogus-leaf-invalid-eku.pem\", False],\n    ),\n)\ndef test_cert_is_leaf(x509_testcase, testcase, valid):\n    cert = x509_testcase(testcase)\n\n    assert utils.cert_is_leaf(cert) is valid\n\n\n@pytest.mark.parametrize(\n    \"testcase\",\n    [\n        \"bogus-root-invalid-ku.pem\",\n        \"bogus-root-missing-ku.pem\",\n        \"bogus-leaf-invalid-ku.pem\",\n        \"bogus-leaf-missing-eku.pem\",\n    ],\n)\ndef test_cert_is_leaf_invalid_states(x509_testcase, testcase):\n    cert = x509_testcase(testcase)\n\n    with pytest.raises(VerificationError):\n        utils.cert_is_leaf(cert)\n\n\n@pytest.mark.parametrize(\n    \"helper\", [utils.cert_is_leaf, utils.cert_is_ca, utils.cert_is_root_ca]\n)\ndef test_cert_is_leaf_invalid_version(helper):\n    cert = pretend.stub(version=x509.Version.v1)\n\n    with pytest.raises(VerificationError, match=\"invalid X.509 version\"):\n        helper(cert)\n"
  },
  {
    "path": "test/unit/test_version.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport sigstore\n\n\ndef test_version():\n    assert isinstance(sigstore.__version__, str)\n"
  },
  {
    "path": "test/unit/verify/__init__.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n"
  },
  {
    "path": "test/unit/verify/test_policy.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport re\n\nimport pretend\nimport pytest\nfrom cryptography.x509 import ExtensionNotFound\n\nfrom sigstore.errors import VerificationError\nfrom sigstore.verify import policy\n\n\nclass TestVerificationPolicy:\n    def test_does_not_init(self):\n        with pytest.raises(TypeError, match=\"Can't instantiate abstract class\"):\n            policy.VerificationPolicy(pretend.stub())\n\n\nclass TestUnsafeNoOp:\n    def test_succeeds(self, monkeypatch):\n        logger = pretend.stub(warning=pretend.call_recorder(lambda s: None))\n        monkeypatch.setattr(policy, \"_logger\", logger)\n\n        policy_ = policy.UnsafeNoOp()\n        policy_.verify(pretend.stub())\n        assert logger.warning.calls == [\n            pretend.call(\n                \"unsafe (no-op) verification policy used! no verification performed!\"\n            )\n        ]\n\n\nclass TestAnyOf:\n    def test_trivially_false(self):\n        policy_ = policy.AnyOf([])\n\n        with pytest.raises(VerificationError, match=\"0 of 0 policies succeeded\"):\n            policy_.verify(pretend.stub())\n\n    def test_fails_no_children_match(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle.txt\")\n        policy_ = policy.AnyOf(\n            [\n                policy.Identity(identity=\"foo\", issuer=\"bar\"),\n                policy.Identity(identity=\"baz\", issuer=\"quux\"),\n            ]\n        )\n\n        with pytest.raises(VerificationError, match=\"0 of 2 policies succeeded\"):\n            policy_.verify(bundle.signing_certificate)\n\n    def test_succeeds(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle.txt\")\n        policy_ = policy.AnyOf(\n            [\n                policy.Identity(identity=\"foo\", issuer=\"bar\"),\n                policy.Identity(identity=\"baz\", issuer=\"quux\"),\n                policy.Identity(\n                    identity=\"a@tny.town\",\n                    issuer=\"https://github.com/login/oauth\",\n                ),\n            ]\n        )\n\n        policy_.verify(bundle.signing_certificate)\n\n\nclass TestAllOf:\n    def test_trivially_false(self):\n        policy_ = policy.AllOf([])\n\n        with pytest.raises(VerificationError, match=\"no child policies to verify\"):\n            policy_.verify(pretend.stub())\n\n    def test_certificate_extension_not_found(self):\n        policy_ = policy.AllOf([policy.Identity(identity=\"foo\", issuer=\"bar\")])\n        cert_ = pretend.stub(\n            extensions=pretend.stub(\n                get_extension_for_oid=pretend.raiser(\n                    ExtensionNotFound(oid=pretend.stub(), msg=pretend.stub())\n                )\n            )\n        )\n\n        reason = re.escape(\n            \"Certificate does not contain OIDCIssuer (1.3.6.1.4.1.57264.1.1) extension\"\n        )\n        with pytest.raises(VerificationError, match=reason):\n            policy_.verify(cert_)\n\n    def test_fails_not_all_children_match(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle.txt\")\n        policy_ = policy.AllOf(\n            [\n                policy.Identity(identity=\"foo\", issuer=\"bar\"),\n                policy.Identity(identity=\"baz\", issuer=\"quux\"),\n                policy.Identity(\n                    identity=\"a@tny.town\",\n                    issuer=\"https://github.com/login/oauth\",\n                ),\n            ]\n        )\n\n        with pytest.raises(\n            VerificationError,\n            match=\"Certificate's OIDCIssuer does not match\",\n        ):\n            policy_.verify(bundle.signing_certificate)\n\n    def test_succeeds(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle.txt\")\n        policy_ = policy.AllOf(\n            [\n                policy.Identity(\n                    identity=\"a@tny.town\",\n                    issuer=\"https://github.com/login/oauth\",\n                ),\n                policy.Identity(\n                    identity=\"a@tny.town\",\n                    issuer=\"https://github.com/login/oauth\",\n                ),\n            ]\n        )\n\n        policy_.verify(bundle.signing_certificate)\n\n\nclass TestIdentity:\n    def test_fails_no_san_match(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle.txt\")\n        policy_ = policy.Identity(\n            identity=\"bad@ident.example.com\",\n            issuer=\"https://github.com/login/oauth\",\n        )\n\n        with pytest.raises(\n            VerificationError,\n            match=\"Certificate's SANs do not match\",\n        ):\n            policy_.verify(bundle.signing_certificate)\n\n\nclass TestSingleExtPolicy:\n    def test_succeeds(self, signing_bundle):\n        _, bundle = signing_bundle(\"bundle_v3_github.whl\")\n\n        verification_policy_extensions = [\n            policy.OIDCIssuer(\"https://token.actions.githubusercontent.com\"),\n            policy.GitHubWorkflowTrigger(\"release\"),\n            policy.GitHubWorkflowSHA(\"d8b4a6445f38c48b9137a8099706d9b8073146e4\"),\n            policy.GitHubWorkflowName(\"release\"),\n            policy.GitHubWorkflowRepository(\"trailofbits/rfc8785.py\"),\n            policy.GitHubWorkflowRef(\"refs/tags/v0.1.2\"),\n            policy.OIDCIssuerV2(\"https://token.actions.githubusercontent.com\"),\n            policy.OIDCBuildSignerURI(\n                \"https://github.com/trailofbits/rfc8785.py/.github/workflows/release.yml@refs/tags/v0.1.2\"\n            ),\n            policy.OIDCBuildSignerDigest(\"d8b4a6445f38c48b9137a8099706d9b8073146e4\"),\n            policy.OIDCRunnerEnvironment(\"github-hosted\"),\n            policy.OIDCSourceRepositoryURI(\"https://github.com/trailofbits/rfc8785.py\"),\n            policy.OIDCSourceRepositoryDigest(\n                \"d8b4a6445f38c48b9137a8099706d9b8073146e4\"\n            ),\n            policy.OIDCSourceRepositoryRef(\"refs/tags/v0.1.2\"),\n            policy.OIDCSourceRepositoryIdentifier(\"768213997\"),\n            policy.OIDCSourceRepositoryOwnerURI(\"https://github.com/trailofbits\"),\n            policy.OIDCSourceRepositoryOwnerIdentifier(\"2314423\"),\n            policy.OIDCBuildConfigURI(\n                \"https://github.com/trailofbits/rfc8785.py/.github/workflows/release.yml@refs/tags/v0.1.2\"\n            ),\n            policy.OIDCBuildConfigDigest(\"d8b4a6445f38c48b9137a8099706d9b8073146e4\"),\n            policy.OIDCBuildTrigger(\"release\"),\n            policy.OIDCRunInvocationURI(\n                \"https://github.com/trailofbits/rfc8785.py/actions/runs/8351058501/attempts/1\"\n            ),\n            policy.OIDCSourceRepositoryVisibility(\"public\"),\n        ]\n\n        policy_ = policy.AllOf(verification_policy_extensions)\n        policy_.verify(bundle.signing_certificate)\n"
  },
  {
    "path": "test/unit/verify/test_verifier.py",
    "content": "# Copyright 2022 The Sigstore Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\nimport hashlib\nimport json\nimport logging\nfrom datetime import datetime, timezone\n\nimport pretend\nimport pytest\nimport rfc3161_client\n\nfrom sigstore._internal.trust import CertificateAuthority\nfrom sigstore.dsse import StatementBuilder, Subject\nfrom sigstore.errors import VerificationError\nfrom sigstore.models import Bundle\nfrom sigstore.verify import policy\nfrom sigstore.verify.verifier import Verifier\n\n\n@pytest.mark.production\ndef test_verifier_production():\n    verifier = Verifier.production()\n    assert verifier is not None\n\n\n@pytest.mark.staging\ndef test_verifier_staging():\n    verifier = Verifier.staging()\n    assert verifier is not None\n\n\n@pytest.mark.staging\ndef test_verifier_one_verification(signing_materials, null_policy):\n    verifier = Verifier.staging()\n\n    (file, bundle) = signing_materials(\"a.txt\", verifier._rekor)\n\n    verifier.verify_artifact(file.read_bytes(), bundle, null_policy)\n\n\n@pytest.mark.staging\ndef test_verifier_inconsistent_log_entry(signing_bundle, null_policy):\n    (file, bundle) = signing_bundle(\"bundle_cve_2022_36056.txt\")\n\n    verifier = Verifier.staging()\n\n    with pytest.raises(\n        VerificationError,\n        match=\"transparency log entry is inconsistent with other materials\",\n    ):\n        verifier.verify_artifact(file.read_bytes(), bundle, null_policy)\n\n\n@pytest.mark.staging\ndef test_verifier_digest_mismatch(signing_bundle, null_policy):\n    \"\"\"The signature is over correct content, but digest documented in bundle is wrong\"\"\"\n    (file, bundle) = signing_bundle(\"bundle.txt\")\n    bundle._inner.message_signature.message_digest.digest = b\"\"\n\n    verifier = Verifier.staging()\n    with pytest.raises(\n        VerificationError,\n        match=\"digest mismatch\",\n    ):\n        verifier.verify_artifact(file.read_bytes(), bundle, null_policy)\n\n\n@pytest.mark.staging\ndef test_verifier_multiple_verifications(signing_materials, null_policy):\n    verifier = Verifier.staging()\n\n    a = signing_materials(\"a.txt\", verifier._rekor)\n    b = signing_materials(\"b.txt\", verifier._rekor)\n\n    for file, bundle in [a, b]:\n        verifier.verify_artifact(file.read_bytes(), bundle, null_policy)\n\n\n@pytest.mark.online\n@pytest.mark.parametrize(\n    \"filename\",\n    (\"bundle.txt\", \"bundle_v3.txt\", \"bundle_v3_alt.txt\", \"staging-rekor-v2.txt\"),\n)\ndef test_verifier_bundle_artifact(signing_bundle, null_policy, filename):\n    (file, bundle) = signing_bundle(filename)\n\n    verifier = Verifier.staging()\n    verifier.verify_artifact(file.read_bytes(), bundle, null_policy)\n\n\n@pytest.mark.online\n@pytest.mark.parametrize(\n    \"filename\",\n    (\"a.dsse.staging-rekor-v2.txt\",),\n)\ndef test_verifier_bundle_dsse(signing_bundle, null_policy, filename):\n    (file, bundle) = signing_bundle(filename)\n\n    verifier = Verifier.staging()\n    verifier.verify_dsse(bundle, null_policy)\n\n\n@pytest.mark.parametrize(\n    \"filename\", (\"bundle.txt\", \"bundle_v3.txt\", \"bundle_v3_alt.txt\")\n)\ndef test_verifier_bundle_offline(signing_bundle, null_policy, filename):\n    (file, bundle) = signing_bundle(filename)\n\n    verifier = Verifier.staging(offline=True)\n    verifier.verify_artifact(file.read_bytes(), bundle, null_policy)\n\n\n@pytest.mark.staging\ndef test_verifier_email_identity(signing_materials):\n    verifier = Verifier.staging()\n\n    (file, bundle) = signing_materials(\"a.txt\", verifier._rekor)\n    policy_ = policy.Identity(\n        identity=\"william@yossarian.net\",\n        issuer=\"https://github.com/login/oauth\",\n    )\n\n    verifier.verify_artifact(\n        file.read_bytes(),\n        bundle,\n        policy_,\n    )\n\n\n@pytest.mark.staging\ndef test_verifier_uri_identity(signing_materials):\n    verifier = Verifier.staging()\n    (file, bundle) = signing_materials(\"c.txt\", verifier._rekor)\n    policy_ = policy.Identity(\n        identity=(\n            \"https://github.com/sigstore/\"\n            \"sigstore-python/.github/workflows/ci.yml@refs/pull/288/merge\"\n        ),\n        issuer=\"https://token.actions.githubusercontent.com\",\n    )\n\n    verifier.verify_artifact(\n        file.read_bytes(),\n        bundle,\n        policy_,\n    )\n\n\n@pytest.mark.staging\ndef test_verifier_policy_check(signing_materials):\n    verifier = Verifier.staging()\n    (file, bundle) = signing_materials(\"a.txt\", verifier._rekor)\n\n    # policy that fails to verify for any given cert.\n    policy_ = pretend.stub(verify=pretend.raiser(VerificationError(\"policy failed\")))\n\n    with pytest.raises(VerificationError, match=\"policy failed\"):\n        verifier.verify_artifact(\n            file.read_bytes(),\n            bundle,\n            policy_,\n        )\n\n\n@pytest.mark.staging\n@pytest.mark.xfail\ndef test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch):\n    # FIXME(jl): can't mock:\n    # - datetime.datetime.utcfromtimestamp: immutable type.\n    # - entry.integrated_time: frozen dataclass.\n    # - Certificate.not_valid_{before,after}: rust FFI.\n    import datetime\n\n    verifier = Verifier.staging()\n\n    bundle: Bundle\n    (file, bundle) = signing_materials(\"a.txt\", verifier._rekor)\n\n    entry = bundle._inner.verification_material.tlog_entries[0]\n    entry.integrated_time = datetime.MINYEAR\n\n    with pytest.raises(VerificationError):\n        verifier.verify_artifact(file.read_bytes(), bundle, null_policy)\n\n\n@pytest.mark.staging\n@pytest.mark.ambient_oidc\ndef test_verifier_dsse_roundtrip(staging):\n    signer_cls, verifier_cls, identity = staging\n\n    ctx = signer_cls()\n    stmt = (\n        StatementBuilder()\n        .subjects(\n            [Subject(name=\"null\", digest={\"sha256\": hashlib.sha256(b\"\").hexdigest()})]\n        )\n        .predicate_type(\"https://cosign.sigstore.dev/attestation/v1\")\n        .predicate(\n            {\n                \"Data\": \"\",\n                \"Timestamp\": \"2023-12-07T00:37:58Z\",\n            }\n        )\n    ).build()\n\n    with ctx.signer(identity) as signer:\n        bundle = signer.sign_dsse(stmt)\n\n    verifier = verifier_cls()\n    payload_type, payload = verifier.verify_dsse(bundle, policy.UnsafeNoOp())\n    assert payload_type == \"application/vnd.in-toto+json\"\n    assert payload == stmt._contents\n\n\nclass TestVerifierWithTimestamp:\n    @pytest.fixture\n    def verifier(self, asset) -> Verifier:\n        \"\"\"Returns a Verifier with Timestamp Authorities set.\"\"\"\n        verifier = Verifier.staging(offline=True)\n        authority = CertificateAuthority.from_json(asset(\"tsa/ca.json\").as_posix())\n        verifier._trusted_root._inner.timestamp_authorities = [authority._inner]\n        return verifier\n\n    def test_verifier_verify_timestamp(self, verifier, asset, null_policy, monkeypatch):\n        # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the\n        # TSA timestamp are required\n        monkeypatch.setattr(\"sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD\", 2)\n\n        verifier.verify_artifact(\n            asset(\"tsa/bundle.txt\").read_bytes(),\n            Bundle.from_json(asset(\"tsa/bundle.txt.sigstore\").read_bytes()),\n            null_policy,\n        )\n\n    def test_verifier_no_validity_end(self, verifier, asset, null_policy):\n        verifier._trusted_root.get_timestamp_authorities()[\n            0\n        ]._inner.valid_for.end = None\n        verifier.verify_artifact(\n            asset(\"tsa/bundle.txt\").read_bytes(),\n            Bundle.from_json(asset(\"tsa/bundle.txt.sigstore\").read_bytes()),\n            null_policy,\n        )\n\n    @pytest.mark.parametrize(\n        \"fields_to_delete\",\n        (\n            [],\n            [\"inclusionPromise\"],\n            # integratedTime is required to verify the inclusionPromise.\n            pytest.param([\"integratedTime\"], marks=pytest.mark.xfail),\n            [\"inclusionPromise\", \"integratedTime\"],\n        ),\n    )\n    def test_verifier_verify_no_inclusion_promise_and_integrated_time(\n        self, verifier, asset, null_policy, fields_to_delete\n    ):\n        \"\"\"\n        Ensure that we can still verify a Bundle with an RFC 3161 timestamp if the SET isn't present.\n\n        There is one exception: When inclusionPromise is present, but integratedTime is not, then we expect a failure\n        because the integratedTime is required to verify the inclusionPromise.\n        \"\"\"\n        bundle_dict = json.loads(asset(\"tsa/bundle.txt.sigstore\").read_bytes())\n        (entry_dict,) = bundle_dict[\"verificationMaterial\"][\"tlogEntries\"]\n        for field in fields_to_delete:\n            del entry_dict[field]\n        # Bundle.from_json() also validates the bundle's layout.\n        bundle = Bundle.from_json(json.dumps(bundle_dict))\n        verifier.verify_artifact(\n            asset(\"tsa/bundle.txt\").read_bytes(),\n            bundle,\n            null_policy,\n        )\n\n    def test_verifier_without_timestamp(\n        self, verifier, asset, null_policy, monkeypatch\n    ):\n        monkeypatch.setattr(verifier, \"_establish_time\", lambda *args: [])\n        with pytest.raises(VerificationError, match=\"not enough sources\"):\n            verifier.verify_artifact(\n                asset(\"tsa/bundle.txt\").read_bytes(),\n                Bundle.from_json(asset(\"tsa/bundle.txt.sigstore\").read_bytes()),\n                null_policy,\n            )\n\n    def test_verifier_too_many_timestamp(self, verifier, asset, null_policy):\n        with pytest.raises(VerificationError, match=\"too many\"):\n            verifier.verify_artifact(\n                asset(\"tsa/bundle.txt\").read_bytes(),\n                Bundle.from_json(\n                    asset(\"tsa/bundle.many_timestamp.sigstore\").read_bytes()\n                ),\n                null_policy,\n            )\n\n    def test_verifier_duplicate_timestamp(self, verifier, asset, null_policy):\n        with pytest.raises(VerificationError, match=\"duplicate\"):\n            verifier.verify_artifact(\n                asset(\"tsa/bundle.txt\").read_bytes(),\n                Bundle.from_json(asset(\"tsa/bundle.duplicate.sigstore\").read_bytes()),\n                null_policy,\n            )\n\n    def test_verifier_outside_validity_range(\n        self, caplog, verifier, asset, null_policy, monkeypatch\n    ):\n        # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the\n        # TSA timestamp are required\n        monkeypatch.setattr(\"sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD\", 2)\n\n        # Set a date before the timestamp range\n        verifier._trusted_root.get_timestamp_authorities()[\n            0\n        ]._inner.valid_for.end = datetime(2024, 10, 31, tzinfo=timezone.utc)\n\n        with caplog.at_level(logging.DEBUG, logger=\"sigstore.verify.verifier\"):\n            with pytest.raises(\n                VerificationError, match=\"not enough sources of verified time\"\n            ):\n                verifier.verify_artifact(\n                    asset(\"tsa/bundle.txt\").read_bytes(),\n                    Bundle.from_json(asset(\"tsa/bundle.txt.sigstore\").read_bytes()),\n                    null_policy,\n                )\n\n        assert (\n            \"Unable to verify Timestamp because not in CA time range.\"\n            == caplog.records[0].message\n        )\n\n    def test_verifier_rfc3161_error(\n        self, verifier, asset, null_policy, caplog, monkeypatch\n    ):\n        # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the\n        # TSA timestamp are required\n        monkeypatch.setattr(\"sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD\", 2)\n\n        def verify_function(*args):\n            raise rfc3161_client.VerificationError()\n\n        monkeypatch.setattr(rfc3161_client.verify._Verifier, \"verify\", verify_function)\n\n        with caplog.at_level(logging.DEBUG, logger=\"sigstore.verify.verifier\"):\n            with pytest.raises(\n                VerificationError, match=\"not enough sources of verified time\"\n            ):\n                verifier.verify_artifact(\n                    asset(\"tsa/bundle.txt\").read_bytes(),\n                    Bundle.from_json(asset(\"tsa/bundle.txt.sigstore\").read_bytes()),\n                    null_policy,\n                )\n\n        assert caplog.records[0].message == \"Unable to verify Timestamp with CA.\"\n\n    def test_verifier_no_authorities(self, asset, null_policy):\n        verifier = Verifier.staging(offline=True)\n        verifier._trusted_root._inner.timestamp_authorities = []\n\n        with pytest.raises(VerificationError, match=\"no Timestamp Authorities\"):\n            verifier.verify_artifact(\n                asset(\"tsa/bundle.txt\").read_bytes(),\n                Bundle.from_json(asset(\"tsa/bundle.txt.sigstore\").read_bytes()),\n                null_policy,\n            )\n\n    def test_late_timestamp(self, caplog, verifier, asset, null_policy, monkeypatch):\n        \"\"\"\n        Ensures that verifying the signing certificate fails because the timestamp\n        is outside the certificate's validity window. The sample bundle\n        \"tsa/bundle.txt.late_timestamp.sigstore\" was generated by adding `time.sleep(12*60)`\n        into `sigstore.sign.Signer._finalize_sign()`, just after the entry is posted to Rekor\n        but before the timestamp is requested.\n        \"\"\"\n        # asset is a rekor v1 bundle: set threshold to 2 so both integrated time and the\n        # TSA timestamp are required\n        monkeypatch.setattr(\"sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD\", 2)\n\n        with pytest.raises(\n            VerificationError, match=\"not enough sources of verified time\"\n        ):\n            verifier.verify_artifact(\n                asset(\"tsa/bundle.txt\").read_bytes(),\n                Bundle.from_json(\n                    asset(\"tsa/bundle.txt.late_timestamp.sigstore\").read_bytes()\n                ),\n                null_policy,\n            )\n\n    def test_verifier_not_enough_timestamp(\n        self, verifier, asset, null_policy, monkeypatch\n    ):\n        # asset is a rekor v1 bundle: set threshold to 3 so integrated time and one\n        # TSA timestamp are not enough\n        monkeypatch.setattr(\"sigstore.verify.verifier.VERIFIED_TIME_THRESHOLD\", 3)\n        with pytest.raises(\n            VerificationError, match=\"not enough sources of verified time\"\n        ):\n            verifier.verify_artifact(\n                asset(\"tsa/bundle.txt\").read_bytes(),\n                Bundle.from_json(asset(\"tsa/bundle.txt.sigstore\").read_bytes()),\n                null_policy,\n            )\n\n    def test_verify_signed_timestamp_regression(self, asset):\n        \"\"\"\n        Ensure we correctly verify a timestamp with no embedded certs.\n\n        This is a regression test for # 1482\n        \"\"\"\n        verifier = Verifier.staging(offline=True)\n        ts = rfc3161_client.decode_timestamp_response(\n            asset(\"tsa/issue1482-timestamp-with-no-cert\").read_bytes()\n        )\n        res = verifier._verify_signed_timestamp(\n            ts, asset(\"tsa/issue1482-message\").read_bytes()\n        )\n        assert res is not None\n"
  }
]