├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── depsreview.yml │ ├── release.yml │ └── scorecard.yml ├── .gitignore ├── .gitleaksignore ├── .pre-commit-config.yaml ├── .rubocop.yml ├── .ruby-version ├── .simplecov ├── CHANGELOG.md ├── CODEOWNERS ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── conformance-entrypoint ├── console ├── rake ├── rubocop ├── setup ├── sigstore-cli ├── smoketest └── tuf-conformance-entrypoint ├── cli ├── .gitignore ├── exe │ └── sigstore-cli ├── lib │ └── sigstore │ │ ├── cli.rb │ │ └── cli │ │ └── id_token.rb └── sigstore-cli.gemspec ├── data └── _store │ ├── prod │ ├── root.json │ └── trusted_root.json │ └── staging │ ├── root.json │ └── trusted_root.json ├── fixtures └── vcr_cassettes │ ├── conformance │ ├── verify_bundle_success.yml │ ├── verify_signature_invalid.yml │ └── verify_signature_success.yml │ └── production.yml ├── lib ├── sigstore.rb └── sigstore │ ├── error.rb │ ├── internal │ ├── json.rb │ ├── key.rb │ ├── keyring.rb │ ├── merkle.rb │ ├── set.rb │ ├── util.rb │ └── x509.rb │ ├── models.rb │ ├── oidc.rb │ ├── policy.rb │ ├── rekor │ ├── checkpoint.rb │ └── client.rb │ ├── signer.rb │ ├── trusted_root.rb │ ├── tuf.rb │ ├── tuf │ ├── config.rb │ ├── error.rb │ ├── file.rb │ ├── keys.rb │ ├── roles.rb │ ├── root.rb │ ├── snapshot.rb │ ├── targets.rb │ ├── timestamp.rb │ ├── trusted_metadata_set.rb │ └── updater.rb │ ├── verifier.rb │ └── version.rb ├── sigstore.gemspec └── test ├── .gitignore ├── sigstore ├── conformance_test.rb ├── data │ ├── transparency │ │ └── merkle │ │ │ └── verify_inclusion.jsonl │ └── x509 │ │ ├── cryptography-scts-tbs-precert.der │ │ └── cryptography-scts.pem ├── internal │ ├── json_test.rb │ ├── keyring_test.rb │ ├── merkle_test.rb │ ├── set_test.rb │ ├── tuf_test.rb │ └── x509_test.rb ├── models_test.rb ├── oidc_test.rb ├── policy_test.rb ├── rekor │ ├── checkpoint_test.rb │ └── client_test.rb ├── transparency_test.rb ├── trusted_root_test.rb ├── tuf │ ├── root_test.rb │ ├── snapshot_test.rb │ ├── targets_test.rb │ ├── timestamp_test.rb │ └── trusted_metadata_set_test.rb ├── verifier_test.rb └── version_test.rb └── test_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: bundler 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | groups: 9 | rubocop: 10 | patterns: 11 | - "rubocop*" 12 | cooldown: 13 | default-days: 7 14 | 15 | - package-ecosystem: github-actions 16 | directory: / 17 | schedule: 18 | interval: weekly 19 | open-pull-requests-limit: 99 20 | rebase-strategy: "disabled" 21 | groups: 22 | actions: 23 | patterns: 24 | - "*" 25 | cooldown: 26 | default-days: 7 27 | 28 | - package-ecosystem: github-actions 29 | directory: .github/actions/upload-coverage/ 30 | schedule: 31 | interval: weekly 32 | open-pull-requests-limit: 99 33 | rebase-strategy: "disabled" 34 | groups: 35 | actions: 36 | patterns: 37 | - "*" 38 | cooldown: 39 | default-days: 7 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | - cron: "0 12 * * *" 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | ruby-versions: 15 | uses: ruby/actions/.github/workflows/ruby_versions.yml@3fbf038d6f0d8043b914f923764c61bc2a114a77 16 | with: 17 | engine: all 18 | min_version: 3.2 19 | 20 | test: 21 | needs: ruby-versions 22 | runs-on: ${{ matrix.os }} 23 | name: Test Ruby ${{ matrix.ruby }} / ${{ matrix.os }} 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 28 | os: [ubuntu-latest] 29 | # os: [ ubuntu-latest, macos-latest, windows-latest ] 30 | # include: 31 | # - { os: windows-latest, ruby: ucrt } 32 | # - { os: windows-latest, ruby: mingw } 33 | # - { os: windows-latest, ruby: mswin } 34 | steps: 35 | - name: Harden Runner 36 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 37 | with: 38 | egress-policy: audit 39 | 40 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | with: 42 | persist-credentials: false 43 | - name: Set up Ruby 44 | uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0 45 | with: 46 | ruby-version: ${{ matrix.ruby }} 47 | bundler-cache: true 48 | 49 | - name: Run the tests 50 | run: bin/rake test 51 | 52 | - name: Upload coverage reports to Codecov 53 | uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 54 | if: ${{ matrix.ruby }} == ${{ fromJson(needs.ruby-versions.outputs.latest) }} && ${{ matrix.os }} == "ubuntu-latest" && always() 55 | with: 56 | token: ${{ secrets.CODECOV_TOKEN }} 57 | slug: sigstore/sigstore-ruby 58 | 59 | sigstore-conformance: 60 | needs: ruby-versions 61 | runs-on: ${{ matrix.os }} 62 | name: Sigstore Ruby ${{ matrix.ruby }} / ${{ matrix.os }} 63 | strategy: 64 | fail-fast: false 65 | matrix: 66 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 67 | os: [ubuntu-latest] 68 | # os: [ ubuntu-latest, macos-latest, windows-latest ] 69 | # include: 70 | # - { os: windows-latest, ruby: ucrt } 71 | # - { os: windows-latest, ruby: mingw } 72 | # - { os: windows-latest, ruby: mswin } 73 | steps: 74 | - name: Harden Runner 75 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 76 | with: 77 | egress-policy: audit 78 | 79 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 80 | with: 81 | persist-credentials: false 82 | - name: Set up Ruby 83 | uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0 84 | with: 85 | ruby-version: ${{ matrix.ruby }} 86 | bundler-cache: true 87 | 88 | - name: Run the conformance tests 89 | uses: sigstore/sigstore-conformance@d658ea74a060aeabae78f8a379167f219dc38c38 # v0.0.16 90 | with: 91 | entrypoint: ${{ github.workspace }}/bin/conformance-entrypoint 92 | xfail: "${{ matrix.ruby != 'head' && matrix.ruby != '3.4' && 'test_verify_rejects_bad_tsa_timestamp' }}" 93 | if: ${{ matrix.os }} == "ubuntu-latest" 94 | - name: Run the conformance tests against staging 95 | uses: sigstore/sigstore-conformance@d658ea74a060aeabae78f8a379167f219dc38c38 # v0.0.16 96 | with: 97 | entrypoint: ${{ github.workspace }}/bin/conformance-entrypoint 98 | xfail: "${{ matrix.ruby != 'head' && matrix.ruby != '3.4' && 'test_verify_rejects_bad_tsa_timestamp' }}" 99 | environment: staging 100 | if: ${{ matrix.os }} == "ubuntu-latest" 101 | 102 | tuf-conformance: 103 | needs: ruby-versions 104 | runs-on: ${{ matrix.os }} 105 | name: TUF Ruby ${{ matrix.ruby }} / ${{ matrix.os }} 106 | strategy: 107 | fail-fast: false 108 | matrix: 109 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 110 | os: [ubuntu-latest] 111 | # os: [ ubuntu-latest, macos-latest, windows-latest ] 112 | # include: 113 | # - { os: windows-latest, ruby: ucrt } 114 | # - { os: windows-latest, ruby: mingw } 115 | # - { os: windows-latest, ruby: mswin } 116 | steps: 117 | - name: Harden Runner 118 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 119 | with: 120 | egress-policy: audit 121 | 122 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 123 | with: 124 | persist-credentials: false 125 | - name: Set up Ruby 126 | uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0 127 | with: 128 | ruby-version: ${{ matrix.ruby }} 129 | bundler-cache: true 130 | 131 | - name: Touch requirements.txt 132 | run: touch requirements.txt 133 | 134 | - name: Write xfails 135 | run: bin/rake bin/tuf-conformance-entrypoint.xfails 136 | 137 | - name: Run the TUF conformance tests 138 | uses: theupdateframework/tuf-conformance@9bfc222a371e30ad5511eb17449f68f855fb9d8f # v2.3.0 139 | with: 140 | entrypoint: ${{ github.workspace }}/bin/tuf-conformance-entrypoint 141 | artifact-name: "test repositories ${{ matrix.ruby }} ${{ matrix.os }}" 142 | if: | 143 | ${{ matrix.os }} == "ubuntu-latest" 144 | 145 | smoketest: 146 | needs: ruby-versions 147 | runs-on: ubuntu-latest 148 | name: Smoketest 149 | permissions: {} 150 | strategy: 151 | fail-fast: false 152 | matrix: 153 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 154 | os: [ubuntu-latest] 155 | steps: 156 | - name: Harden Runner 157 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 158 | with: 159 | egress-policy: audit 160 | 161 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 162 | with: 163 | persist-credentials: false 164 | - name: Set up Ruby 165 | uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0 166 | with: 167 | ruby-version: ${{ fromJson(needs.ruby-versions.outputs.latest) }} 168 | bundler-cache: true 169 | - name: Build the gem 170 | run: bin/rake build 171 | - name: List built gems 172 | id: list-gems 173 | run: | 174 | echo "gems=$(find pkg -type f -name '*.gem' -print0 | xargs -0 jq --compact-output --null-input --args '[$ARGS.positional[]]')" >> $GITHUB_OUTPUT 175 | - name: Fetch testing OIDC token 176 | uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@4a8befcc16064dac9e97f210948d226e5c869bdc # v1.0.0 177 | - name: Run the smoketest 178 | run: | 179 | ./bin/smoketest ${BUILT_GEMS} 180 | env: 181 | BUILT_GEMS: ${{ join(fromJson(steps.list-gems.outputs.gems), ' ') }} 182 | OIDC_TOKEN_FILE: ./oidc-token.txt 183 | SIGSTORE_CERT_IDENTITY: https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main 184 | 185 | all-tests-pass: 186 | if: always() 187 | 188 | needs: 189 | - test 190 | - sigstore-conformance 191 | - tuf-conformance 192 | 193 | runs-on: ubuntu-latest 194 | 195 | steps: 196 | - name: Harden Runner 197 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 198 | with: 199 | egress-policy: audit 200 | 201 | - name: check test jobs 202 | uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 203 | with: 204 | jobs: ${{ toJSON(needs) }} 205 | 206 | lint: 207 | needs: ruby-versions 208 | runs-on: ubuntu-latest 209 | name: Lint 210 | steps: 211 | - name: Harden Runner 212 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 213 | with: 214 | egress-policy: audit 215 | 216 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 217 | with: 218 | persist-credentials: false 219 | - name: Set up Ruby 220 | uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0 221 | with: 222 | ruby-version: ${{ fromJson(needs.ruby-versions.outputs.latest) }} 223 | bundler-cache: true 224 | - name: Run the linter 225 | run: bin/rubocop 226 | 227 | zizmor: 228 | name: zizmor 229 | runs-on: ubuntu-latest 230 | permissions: 231 | security-events: write 232 | # required for workflows in private repositories 233 | contents: read 234 | actions: read 235 | steps: 236 | - name: Checkout repository 237 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 238 | with: 239 | persist-credentials: false 240 | 241 | - name: Install the latest version of uv 242 | uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 243 | 244 | - name: Run zizmor 🌈 245 | run: uvx zizmor --format sarif . > results.sarif 246 | 247 | env: 248 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 249 | 250 | - name: Upload SARIF file 251 | uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 252 | with: 253 | sarif_file: results.sarif 254 | category: zizmor 255 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["main"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["main"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["ruby"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Harden Runner 44 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 45 | with: 46 | egress-policy: audit 47 | 48 | - name: Checkout repository 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 50 | with: 51 | persist-credentials: false 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 63 | # If this step fails, then you should remove it and run the build manually (see below) 64 | - name: Autobuild 65 | uses: github/codeql-action/autobuild@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 66 | 67 | # ℹ️ Command-line programs to run using the OS shell. 68 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 69 | 70 | # If the Autobuild fails above, remove it and uncomment the following three lines. 71 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 72 | 73 | # - run: | 74 | # echo "Run, Build Application using script" 75 | # ./location_of_script_within_repo/buildscript.sh 76 | 77 | - name: Perform CodeQL Analysis 78 | uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 79 | with: 80 | category: "/language:${{matrix.language}}" 81 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: "Dependency Review" 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 21 | with: 22 | egress-policy: audit 23 | 24 | - name: "Checkout Repository" 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | persist-credentials: false 28 | - name: "Dependency Review" 29 | uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 30 | -------------------------------------------------------------------------------- /.github/workflows/depsreview.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2022 The Sigstore Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | name: 'Dependency Review' 16 | on: [pull_request] 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | dependency-review: 23 | name: License and Vulnerability Scan 24 | uses: sigstore/community/.github/workflows/reusable-dependency-review.yml@9b1b5aca605f92ec5b1bf3681b1e61b3dbc420cc 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | name: Build and sign artifacts 14 | runs-on: ubuntu-latest 15 | permissions: 16 | id-token: write 17 | outputs: 18 | hashes: ${{ steps.hash.outputs.hashes }} 19 | built-gems: ${{ steps.list-gems.outputs.gems }} 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | 25 | - uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0 26 | with: 27 | # NOTE: We intentionally don't use a cache in the release step, 28 | # to reduce the risk of cache poisoning. 29 | ruby-version: "3.3" 30 | bundler-cache: false 31 | 32 | - name: deps 33 | run: bundle install --jobs 4 --retry 3 34 | 35 | - name: Set source date epoch 36 | run: | 37 | # Set SOURCE_DATE_EPOCH to the commit date of the last commit. 38 | export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) 39 | echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" >> $GITHUB_ENV 40 | 41 | - name: build 42 | run: bin/rake build 43 | 44 | - name: List built gems 45 | id: list-gems 46 | run: | 47 | echo "gems=$(find pkg -type f -name '*.gem' -print0 | xargs -0 jq --compact-output --null-input --args '[$ARGS.positional[]]')" >> $GITHUB_OUTPUT 48 | 49 | - name: Check release and tag name match built version 50 | run: | 51 | for gem in ${BUILT_GEMS}; do 52 | gemspec_version=$(gem spec ${gem} version | ruby -ryaml -e 'puts YAML.safe_load(ARGF.read, permitted_classes: [Gem::Version])') 53 | if [ "${RELEASE_TAG_NAME}" != "v${gemspec_version}" ]; then 54 | echo "Release tag name '${RELEASE_TAG_NAME}' does not match gemspec version 'v${gemspec_version}'" 55 | exit 1 56 | fi 57 | done 58 | env: 59 | RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} 60 | BUILT_GEMS: ${{ join(fromJson(steps.list-gems.outputs.gems), ' ') }} 61 | 62 | - name: sign 63 | run: | 64 | ./bin/smoketest ${BUILT_GEMS} 65 | env: 66 | BUILT_GEMS: ${{ join(fromJson(steps.list-gems.outputs.gems), ' ') }} 67 | SIGSTORE_CERT_IDENTITY: ${{ github.server_url }}/${{ github.repository }}/.github/workflows/release.yml@${{ github.ref }} 68 | 69 | - name: Generate hashes for provenance 70 | shell: bash 71 | id: hash 72 | working-directory: pkg 73 | run: | 74 | # sha256sum generates sha256 hash for all artifacts. 75 | # base64 -w0 encodes to base64 and outputs on a single line. 76 | # sha256sum artifact1 artifact2 ... | base64 -w0 77 | echo "hashes=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT 78 | 79 | - name: Save hashes 80 | run: echo "$HASHES" | base64 -d > pkg/sha256sum.txt 81 | env: 82 | HASHES: ${{ steps.hash.outputs.hashes }} 83 | 84 | - name: Upload built packages 85 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 86 | with: 87 | name: built-packages 88 | path: ./pkg/ 89 | if-no-files-found: warn 90 | 91 | - name: Upload smoketest-artifacts 92 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 93 | with: 94 | name: smoketest-artifacts 95 | path: smoketest-artifacts/ 96 | if-no-files-found: warn 97 | 98 | generate-provenance: 99 | needs: [build] 100 | name: Generate build provenance 101 | permissions: 102 | actions: read # To read the workflow path. 103 | id-token: write # To sign the provenance. 104 | contents: write # To add assets to a release. 105 | # Currently this action needs to be referred by tag. More details at: 106 | # https://github.com/slsa-framework/slsa-github-generator#verification-of-provenance 107 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 108 | with: 109 | provenance-name: provenance-sigstore-${{ github.event.release.tag_name }}.intoto.jsonl 110 | base64-subjects: "${{ needs.build.outputs.hashes }}" 111 | upload-assets: true 112 | 113 | release-rubygems: 114 | needs: [build, generate-provenance] 115 | runs-on: ubuntu-latest 116 | permissions: 117 | # Used to authenticate to RubyGems.org via OIDC. 118 | id-token: write 119 | strategy: 120 | matrix: 121 | built-gem: ${{ fromJson(needs.build.outputs.built-gems) }} 122 | concurrency: 123 | group: release-rubygems 124 | name: Publish ${{ matrix.built-gem }} to RubyGems 125 | steps: 126 | - name: Download artifacts directories # goes to current working directory 127 | uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 128 | 129 | - name: Set up Ruby 130 | uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0 131 | with: 132 | ruby-version: "3.3" 133 | bundler-cache: false 134 | 135 | - name: Clone rubygems HEAD 136 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 137 | with: 138 | repository: rubygems/rubygems 139 | persist-credentials: false 140 | fetch-depth: 0 141 | ref: a5412d9a0e358893e20ac69a4c6c0c2bac59d888 142 | path: rubygems 143 | 144 | - name: Install rubygems HEAD 145 | run: ruby setup.rb 146 | working-directory: rubygems 147 | 148 | - name: Configure RubyGems credentials 149 | uses: rubygems/configure-rubygems-credentials@f456a002d58f0de60b44383d10ae82316b18a166 # main 150 | with: 151 | trusted-publisher: true 152 | 153 | - name: publish 154 | run: | 155 | gem push "built-packages/$(basename $BUILT_GEM)" --attestation "smoketest-artifacts/$(basename $BUILT_GEM).sigstore.json" 156 | env: 157 | BUILT_GEM: ${{ matrix.built-gem }} 158 | 159 | release-github: 160 | needs: [build, generate-provenance] 161 | runs-on: ubuntu-latest 162 | permissions: 163 | # Needed to upload release assets. 164 | contents: write 165 | steps: 166 | - name: Download artifacts directories # goes to current working directory 167 | uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 168 | 169 | - name: Upload artifacts to github 170 | # Confusingly, this action also supports updating releases, not 171 | # just creating them. This is what we want here, since we've manually 172 | # created the release that triggered the action. 173 | uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 174 | with: 175 | # smoketest-artifacts/ contains the signatures and certificates. 176 | files: | 177 | built-packages/* 178 | smoketest-artifacts/* 179 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: "37 2 * * 0" 14 | push: 15 | branches: ["main"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: 19 | contents: read 20 | security-events: read 21 | actions: read 22 | 23 | jobs: 24 | analysis: 25 | name: Scorecard analysis 26 | runs-on: ubuntu-latest 27 | permissions: 28 | # Needed to upload the results to code-scanning dashboard. 29 | security-events: write 30 | # Needed to publish results and get a badge (see publish_results below). 31 | id-token: write 32 | # Uncomment the permissions below if installing in a private repository. 33 | # contents: read 34 | # actions: read 35 | 36 | steps: 37 | - name: Harden Runner 38 | uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 39 | with: 40 | egress-policy: audit 41 | 42 | - name: "Checkout code" 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | with: 45 | persist-credentials: false 46 | 47 | - name: "Run analysis" 48 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 49 | with: 50 | results_file: results.sarif 51 | results_format: sarif 52 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 53 | # - you want to enable the Branch-Protection check on a *public* repository, or 54 | # - you are installing Scorecard on a *private* repository 55 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 56 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 57 | 58 | # Public repositories: 59 | # - Publish results to OpenSSF REST API for easy access by consumers 60 | # - Allows the repository to include the Scorecard badge. 61 | # - See https://github.com/ossf/scorecard-action#publishing-results. 62 | # For private repositories: 63 | # - `publish_results` will always be set to `false`, regardless 64 | # of the value entered here. 65 | publish_results: true 66 | 67 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 68 | # format to the repository Actions tab. 69 | - name: "Upload artifact" 70 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 71 | with: 72 | name: SARIF file 73 | path: results.sarif 74 | retention-days: 5 75 | 76 | # Upload the results to GitHub's code scanning dashboard (optional). 77 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 78 | - name: "Upload to code-scanning" 79 | uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 80 | with: 81 | sarif_file: results.sarif 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /bin/tuf-conformance-entrypoint.xfails 10 | -------------------------------------------------------------------------------- /.gitleaksignore: -------------------------------------------------------------------------------- 1 | fixtures/vcr_cassettes/conformance/verify_bundle_success.yml:generic-api-key:115 2 | fixtures/vcr_cassettes/production.yml:generic-api-key:115 3 | fixtures/vcr_cassettes/conformance/verify_bundle_success.yml:generic-api-key:200 4 | fixtures/vcr_cassettes/production.yml:generic-api-key:200 5 | fixtures/vcr_cassettes/conformance/verify_signature_invalid.yml:generic-api-key:115 6 | fixtures/vcr_cassettes/conformance/verify_signature_invalid.yml:generic-api-key:200 7 | fixtures/vcr_cassettes/conformance/verify_signature_invalid.yml:generic-api-key:320 8 | fixtures/vcr_cassettes/conformance/verify_signature_invalid.yml:generic-api-key:324 9 | fixtures/vcr_cassettes/conformance/verify_signature_invalid.yml:generic-api-key:328 10 | fixtures/vcr_cassettes/conformance/verify_signature_invalid.yml:generic-api-key:332 11 | fixtures/vcr_cassettes/conformance/verify_signature_invalid.yml:generic-api-key:336 12 | fixtures/vcr_cassettes/production.yml:generic-api-key:320 13 | fixtures/vcr_cassettes/production.yml:generic-api-key:324 14 | fixtures/vcr_cassettes/production.yml:generic-api-key:328 15 | fixtures/vcr_cassettes/production.yml:generic-api-key:332 16 | fixtures/vcr_cassettes/production.yml:generic-api-key:336 17 | fixtures/vcr_cassettes/conformance/verify_bundle_success.yml:generic-api-key:320 18 | fixtures/vcr_cassettes/conformance/verify_bundle_success.yml:generic-api-key:324 19 | fixtures/vcr_cassettes/conformance/verify_bundle_success.yml:generic-api-key:328 20 | fixtures/vcr_cassettes/conformance/verify_bundle_success.yml:generic-api-key:332 21 | fixtures/vcr_cassettes/conformance/verify_bundle_success.yml:generic-api-key:336 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/gitleaks/gitleaks 3 | rev: v8.18.4 4 | hooks: 5 | - id: gitleaks 6 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 7 | rev: 3.0.0 8 | hooks: 9 | - id: bundler-audit 10 | - id: rubocop 11 | - id: shellcheck 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v4.6.0 14 | hooks: 15 | - id: end-of-file-fixer 16 | - id: trailing-whitespace 17 | exclude: ^(data/|test/sigstore/data/) 18 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 3.2 3 | NewCops: enable 4 | Exclude: 5 | - "test/sigstore-conformance/**/*" 6 | - "test/tuf-conformance/**/*" 7 | - "node_modules/**/*" 8 | - "tmp/**/*" 9 | - "vendor/**/*" 10 | - ".git/**/*" 11 | 12 | require: 13 | - rubocop-performance 14 | - rubocop-rake 15 | 16 | Style/StringLiterals: 17 | Enabled: true 18 | EnforcedStyle: double_quotes 19 | 20 | Style/StringLiteralsInInterpolation: 21 | Enabled: true 22 | EnforcedStyle: double_quotes 23 | 24 | Layout/LineLength: 25 | Max: 120 26 | 27 | Metrics: 28 | Enabled: false 29 | 30 | Style/Documentation: 31 | Enabled: false 32 | 33 | Style/ClassAndModuleChildren: 34 | Enabled: false 35 | 36 | Style/ImplicitRuntimeError: 37 | Enabled: true 38 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.2 2 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | SimpleCov.root(__dir__) 4 | 5 | if ENV["COVERAGE"] 6 | SimpleCov.start do 7 | enable_coverage :branch unless RUBY_ENGINE == "truffleruby" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.1.1] - 2024-10-18 2 | 3 | - Fix release automation 4 | 5 | ## [0.1.0] - 2024-10-18 6 | 7 | - Initial release 8 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @sigstore/codeowners-sigstore-ruby 2 | 3 | # The CODEOWNERS are managed via a GitHub team, but the current list is (in alphabetical order): 4 | 5 | # segiddins 6 | # woodruffw 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in sigstore.gemspec 6 | gemspec 7 | gemspec path: "cli" 8 | 9 | gem "cgi", "~> 0.5.0" # Used by webmock 10 | gem "rake", "~> 13.3" 11 | gem "rubocop", "~> 1.67" 12 | gem "rubocop-performance", "~> 1.23" 13 | gem "rubocop-rake", "~> 0.6.0" 14 | gem "simplecov", "~> 0.22.0" 15 | gem "test-unit", "~> 3.7" 16 | gem "thor", "~> 1.3" 17 | gem "timecop", "~> 0.9.10" 18 | gem "vcr", "~> 6.3" 19 | gem "webmock", "~> 3.25" 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | sigstore (0.2.2) 5 | logger 6 | net-http 7 | protobug_sigstore_protos (~> 0.1.0) 8 | uri 9 | 10 | PATH 11 | remote: cli 12 | specs: 13 | sigstore-cli (0.2.2) 14 | sigstore (= 0.2.2) 15 | thor 16 | 17 | GEM 18 | remote: https://rubygems.org/ 19 | specs: 20 | addressable (2.8.7) 21 | public_suffix (>= 2.0.2, < 7.0) 22 | ast (2.4.2) 23 | base64 (0.3.0) 24 | bigdecimal (3.1.9) 25 | bigdecimal (3.1.9-java) 26 | cgi (0.5.0) 27 | cgi (0.5.0-java) 28 | crack (1.0.0) 29 | bigdecimal 30 | rexml 31 | docile (1.4.0) 32 | hashdiff (1.1.2) 33 | json (2.10.2) 34 | json (2.10.2-java) 35 | language_server-protocol (3.17.0.4) 36 | logger (1.7.0) 37 | net-http (0.6.0) 38 | uri 39 | parallel (1.26.3) 40 | parser (3.3.7.1) 41 | ast (~> 2.4.1) 42 | racc 43 | power_assert (2.0.5) 44 | protobug (0.1.0) 45 | protobug_googleapis_field_behavior_protos (0.1.0) 46 | protobug (= 0.1.0) 47 | protobug_well_known_protos (= 0.1.0) 48 | protobug_sigstore_protos (0.1.0) 49 | protobug (= 0.1.0) 50 | protobug_googleapis_field_behavior_protos (= 0.1.0) 51 | protobug_well_known_protos (= 0.1.0) 52 | protobug_well_known_protos (0.1.0) 53 | protobug (= 0.1.0) 54 | public_suffix (6.0.1) 55 | racc (1.8.1) 56 | racc (1.8.1-java) 57 | rainbow (3.1.1) 58 | rake (13.3.0) 59 | regexp_parser (2.10.0) 60 | rexml (3.4.2) 61 | rubocop (1.67.0) 62 | json (~> 2.3) 63 | language_server-protocol (>= 3.17.0) 64 | parallel (~> 1.10) 65 | parser (>= 3.3.0.2) 66 | rainbow (>= 2.2.2, < 4.0) 67 | regexp_parser (>= 2.4, < 3.0) 68 | rubocop-ast (>= 1.32.2, < 2.0) 69 | ruby-progressbar (~> 1.7) 70 | unicode-display_width (>= 2.4.0, < 3.0) 71 | rubocop-ast (1.38.0) 72 | parser (>= 3.3.1.0) 73 | rubocop-performance (1.23.1) 74 | rubocop (>= 1.48.1, < 2.0) 75 | rubocop-ast (>= 1.31.1, < 2.0) 76 | rubocop-rake (0.6.0) 77 | rubocop (~> 1.0) 78 | ruby-progressbar (1.13.0) 79 | simplecov (0.22.0) 80 | docile (~> 1.1) 81 | simplecov-html (~> 0.11) 82 | simplecov_json_formatter (~> 0.1) 83 | simplecov-html (0.12.3) 84 | simplecov_json_formatter (0.1.4) 85 | test-unit (3.7.0) 86 | power_assert 87 | thor (1.3.2) 88 | timecop (0.9.10) 89 | unicode-display_width (2.6.0) 90 | uri (1.0.4) 91 | vcr (6.3.1) 92 | base64 93 | webmock (3.25.1) 94 | addressable (>= 2.8.0) 95 | crack (>= 0.3.2) 96 | hashdiff (>= 0.4.0, < 2.0.0) 97 | 98 | PLATFORMS 99 | arm64-darwin 100 | arm64-linux 101 | java 102 | ruby 103 | universal-java-1.8 104 | universal-java-19 105 | universal-java-24 106 | x86_64-linux 107 | 108 | DEPENDENCIES 109 | cgi (~> 0.5.0) 110 | rake (~> 13.3) 111 | rubocop (~> 1.67) 112 | rubocop-performance (~> 1.23) 113 | rubocop-rake (~> 0.6.0) 114 | sigstore! 115 | sigstore-cli! 116 | simplecov (~> 0.22.0) 117 | test-unit (~> 3.7) 118 | thor (~> 1.3) 119 | timecop (~> 0.9.10) 120 | vcr (~> 6.3) 121 | webmock (~> 3.25) 122 | 123 | CHECKSUMS 124 | addressable (2.8.7) sha256=462986537cf3735ab5f3c0f557f14155d778f4b43ea4f485a9deb9c8f7c58232 125 | ast (2.4.2) sha256=1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12 126 | base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b 127 | bigdecimal (3.1.9) sha256=2ffc742031521ad69c2dfc815a98e426a230a3d22aeac1995826a75dabfad8cc 128 | bigdecimal (3.1.9-java) sha256=dd9b8f7c870664cd9538a1325ce385ba57a6627969177258c4f0e661a7be4456 129 | cgi (0.5.0) sha256=fe99f65bb2c146e294372ebb27602adbc3b4c008e9ea7038c6bd48c1ec9759da 130 | cgi (0.5.0-java) sha256=e39b3e7d74961e5474bb18d9bbfd7308f01cc7e9f1cfe7f379281c5d0c26f90d 131 | crack (1.0.0) sha256=c83aefdb428cdc7b66c7f287e488c796f055c0839e6e545fec2c7047743c4a49 132 | docile (1.4.0) sha256=5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3 133 | hashdiff (1.1.2) sha256=2c30eeded6ed3dce8401d2b5b99e6963fe5f14ed85e60dd9e33c545a44b71a77 134 | json (2.10.2) sha256=34e0eada93022b2a0a3345bb0b5efddb6e9ff5be7c48e409cfb54ff8a36a8b06 135 | json (2.10.2-java) sha256=fe31faac61ea21ea1448c35450183f84e85c2b94cc6522c241959ba9d1362006 136 | language_server-protocol (3.17.0.4) sha256=c484626478664fd13482d8180947c50a8590484b1258b99b7aedb3b69df89669 137 | logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 138 | net-http (0.6.0) sha256=9621b20c137898af9d890556848c93603716cab516dc2c89b01a38b894e259fb 139 | parallel (1.26.3) sha256=d86babb7a2b814be9f4b81587bf0b6ce2da7d45969fab24d8ae4bf2bb4d4c7ef 140 | parser (3.3.7.1) sha256=7dbe61618025519024ac72402a6677ead02099587a5538e84371b76659e6aca1 141 | power_assert (2.0.5) sha256=63b511b85bb8ea57336d25156864498644f5bbf028699ceda27949e0125bc323 142 | protobug (0.1.0) sha256=5bf1356cedf99dcf311890743b78f5e602f62ca703e574764337f1996b746bf2 143 | protobug_googleapis_field_behavior_protos (0.1.0) sha256=db48ef6a5913b2355b4a6931ab400a9e3e995fb48499977a3ad0be6365f9e265 144 | protobug_sigstore_protos (0.1.0) sha256=4ad1eebaf6454131b6f432dda50ad0e513773613474b92470847614a5acacce1 145 | protobug_well_known_protos (0.1.0) sha256=356757f562453bb34a28f12e8e9fa357346cca35a6807a549837c3fe256bb5b3 146 | public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f 147 | racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f 148 | racc (1.8.1-java) sha256=54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98 149 | rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a 150 | rake (13.3.0) sha256=96f5092d786ff412c62fde76f793cc0541bd84d2eb579caa529aa8a059934493 151 | regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61 152 | rexml (3.4.2) sha256=1384268554a37af5da5279431ca3f2f37d46f09ffdd6c95e17cc84c83ea7c417 153 | rubocop (1.67.0) sha256=8ccca7226e76d0a9974af960ea446d1fb38adf0c491214294e2fed75a85c378c 154 | rubocop-ast (1.38.0) sha256=4fdf6792fe443a9a18acb12dbc8225d0d64cd1654e41fedb30e79c18edbb26ae 155 | rubocop-performance (1.23.1) sha256=f22f86a795f5e6a6180aac2c6fc172534b173a068d6ed3396d6460523e051b82 156 | rubocop-rake (0.6.0) sha256=56b6f22189af4b33d4f4e490a555c09f1281b02f4d48c3a61f6e8fe5f401d8db 157 | ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 158 | sigstore (0.2.2) 159 | sigstore-cli (0.2.2) 160 | simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 161 | simplecov-html (0.12.3) sha256=4b1aad33259ffba8b29c6876c12db70e5750cb9df829486e4c6e5da4fa0aa07b 162 | simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 163 | test-unit (3.7.0) sha256=2b5745498c848768e1774acb63e3806d3bb47e2943bd91cc9bf559b4c6d4faa1 164 | thor (1.3.2) sha256=eef0293b9e24158ccad7ab383ae83534b7ad4ed99c09f96f1a6b036550abbeda 165 | timecop (0.9.10) sha256=12ba45ce57cdcf6b1043cb6cdffa6381fd89ce10d369c28a7f6f04dc1b0cd8eb 166 | unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a 167 | uri (1.0.4) sha256=34485d137c079f8753a0ca1d883841a7ba2e5fae556e3c30c2aab0dde616344b 168 | vcr (6.3.1) sha256=37b56e157e720446a3f4d2d39919cabef8cb7b6c45936acffd2ef8229fec03ed 169 | webmock (3.25.1) sha256=ab9d5d9353bcbe6322c83e1c60a7103988efc7b67cd72ffb9012629c3d396323 170 | 171 | BUNDLED WITH 172 | 2.6.9 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sigstore 2 | 3 | This is a pure Ruby implementation of the `sigstore verify` command from the [sigstore/cosign](https://sigstore.dev/projects/cosign) project. It is intended to be used as a library in other Ruby projects or directly through a new `gem` subcommand. The project also contains a TUF client implementation, given TUF is a part of the sigstore verification flow. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | $ gem sigstore_cosign_verify_bundle --bundle a.txt.sigstore \ 9 | --certificate-identity https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main \ 10 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 11 | a.txt 12 | ``` 13 | 14 | ## Development 15 | 16 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test-unit` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 17 | 18 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 19 | 20 | ## Contributing 21 | 22 | Bug reports and pull requests are welcome on GitHub at . 23 | 24 | ## License 25 | 26 | The gem is available as open source under the terms of the [Apache 2](https://opensource.org/licenses/Apache-2.0). 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | directory "pkg" 7 | namespace "cli" do 8 | Bundler::GemHelper.install_tasks(dir: "cli") 9 | task build: "pkg" do # rubocop:disable Rake/Desc 10 | FileUtils.cp_r FileList["cli/pkg/*"], "pkg" 11 | end 12 | end 13 | task "build" => "cli:build" # rubocop:disable Rake/Desc 14 | 15 | Rake::TestTask.new(:test) do |t| 16 | t.libs << "test" 17 | t.test_files = FileList["test/**/*_test.rb"] 18 | end 19 | 20 | require "rubocop/rake_task" 21 | 22 | RuboCop::RakeTask.new 23 | 24 | task default: %i[test conformance_staging conformance conformance_tuf rubocop] 25 | 26 | require "openssl" 27 | # Checks for https://github.com/ruby/openssl/pull/770 28 | xfail = OpenSSL::X509::Store.new.instance_variable_defined?(:@time) ? "test_verify_rejects_bad_tsa_timestamp" : "" 29 | 30 | desc "Run the conformance tests" 31 | task conformance: %w[conformance:setup] do 32 | sh({ "GHA_SIGSTORE_CONFORMANCE_XFAIL" => xfail }, 33 | File.expand_path("test/sigstore-conformance/env/bin/pytest"), "test", 34 | "--entrypoint=#{File.join(__dir__, "bin", "conformance-entrypoint")}", 35 | chdir: "test/sigstore-conformance") 36 | end 37 | 38 | desc "Run the conformance tests against staging" 39 | task conformance_staging: %w[conformance:setup] do 40 | sh({ "GHA_SIGSTORE_CONFORMANCE_XFAIL" => xfail }, 41 | File.expand_path("test/sigstore-conformance/env/bin/pytest"), "test", 42 | "--entrypoint=#{File.join(__dir__, "bin", "conformance-entrypoint")}", 43 | "--staging", 44 | chdir: "test/sigstore-conformance") 45 | end 46 | 47 | desc "Run the TUF conformance tests" 48 | task conformance_tuf: %w[tuf_conformance:setup] do 49 | sh("env/bin/pytest", "tuf_conformance", "--entrypoint", File.expand_path("bin/tuf-conformance-entrypoint"), 50 | chdir: "test/tuf-conformance") 51 | end 52 | 53 | namespace :conformance do 54 | file "test/sigstore-conformance/env/pyvenv.cfg" => :sigstore_conformance do 55 | sh "make", "dev", chdir: "test/sigstore-conformance" 56 | end 57 | task setup: "test/sigstore-conformance/env/pyvenv.cfg" # rubocop:disable Rake/Desc 58 | end 59 | 60 | task :find_action_versions do # rubocop:disable Rake/Desc 61 | require "yaml" 62 | gh = YAML.load_file(".github/workflows/ci.yml") 63 | actions = gh.fetch("jobs").flat_map { |_, job| job.fetch("steps", []).filter_map { |step| step.fetch("uses", nil) } } 64 | .uniq.map { |x| x.split("@", 2) } 65 | .group_by(&:first).transform_values { |v| v.map(&:last) } 66 | if actions.any? { |_, v| v.size > 1 } 67 | raise StandardError, "conflicts: #{actions.select { |_, v| v.size > 1 }.inspect}" 68 | end 69 | 70 | @action_versions = actions.transform_values(&:first) 71 | end 72 | 73 | task test: %w[sigstore_conformance] 74 | 75 | desc "Update the vendored data files" 76 | task :update_data do 77 | require "sigstore" 78 | require "sigstore/trusted_root" 79 | { 80 | prod: Sigstore::TUF::DEFAULT_TUF_URL, 81 | staging: Sigstore::TUF::STAGING_TUF_URL 82 | }.each do |name, url| 83 | Dir.mktmpdir do |dir| 84 | updater = Sigstore::TUF::TrustUpdater.new(url, false, metadata_dir: dir, targets_dir: dir).updater 85 | updater.refresh 86 | updater.download_target(updater.get_targetinfo("trusted_root.json")) 87 | cp File.join(dir, "trusted_root.json"), "data/_store/#{name}/trusted_root.json" 88 | cp File.join(dir, "root.json"), "data/_store/#{name}/root.json" 89 | end 90 | end 91 | end 92 | 93 | require "open3" 94 | 95 | class GitRepo < Rake::Task 96 | attr_accessor :path, :url 97 | attr_writer :commit 98 | 99 | include FileUtils 100 | 101 | def initialize(*) 102 | super 103 | 104 | @actions << method(:clone_repo) 105 | @actions << method(:checkout) 106 | end 107 | 108 | def needed? 109 | !correct_remote? || !correct_commit? 110 | end 111 | 112 | def correct_remote? 113 | return false unless File.directory?(@path) 114 | 115 | out, status = Open3.capture2(*%w[git remote get-url origin], chdir: path) 116 | status.success? && out.strip == url 117 | end 118 | 119 | def correct_commit? 120 | head, status = Open3.capture2(*%w[git rev-parse HEAD], chdir: path) 121 | head.strip! 122 | return true if status.success? && head == commit 123 | 124 | desired, status = Open3.capture2(*%w[git rev-parse], "#{commit}^{commit}", "--", chdir: path) 125 | desired.strip! 126 | status.success? && desired == head 127 | end 128 | 129 | def clone_repo(_, _) 130 | return if correct_remote? 131 | 132 | rm_rf path 133 | sh "git", "clone", url, path 134 | end 135 | 136 | def checkout(_, _) 137 | return if correct_commit? 138 | 139 | sh "git", "-C", path, "switch", "--detach", commit do |ok, _| 140 | unless ok 141 | sh "git", "-C", path, "fetch", "origin", "#{commit}:#{commit}" 142 | sh "git", "-C", path, "switch", "--detach", commit 143 | end 144 | end 145 | end 146 | 147 | def commit 148 | case @commit 149 | when String 150 | @commit 151 | when ->(c) { c.respond_to?(:call) } 152 | @commit.call 153 | else 154 | raise StandardError, "unexpected commit type: #{@commit.inspect}" 155 | end 156 | end 157 | end 158 | 159 | GitRepo.define_task(sigstore_conformance: %w[find_action_versions]).tap do |task| 160 | task.path = "test/sigstore-conformance" 161 | task.url = "https://github.com/sigstore/sigstore-conformance.git" 162 | task.commit = -> { @action_versions.fetch("sigstore/sigstore-conformance") } 163 | end 164 | 165 | GitRepo.define_task(tuf_conformance: %w[find_action_versions]).tap do |task| 166 | task.path = "test/tuf-conformance" 167 | task.url = "https://github.com/theupdateframework/tuf-conformance.git" 168 | task.commit = -> { @action_versions.fetch("theupdateframework/tuf-conformance") } 169 | end 170 | 171 | namespace :tuf_conformance do 172 | file "bin/tuf-conformance-entrypoint.xfails" do |t| 173 | if RUBY_ENGINE == "jruby" 174 | File.write(t.name, <<~TXT) 175 | test_keytype_and_scheme[rsa/rsassa-pss-sha256] 176 | test_keytype_and_scheme[ed25519/ed25519] 177 | TXT 178 | else 179 | File.write(t.name, "") 180 | end 181 | end 182 | file "test/tuf-conformance/env/pyvenv.cfg" => :tuf_conformance do 183 | sh "make", "dev", chdir: "test/tuf-conformance" 184 | end 185 | task setup: %w[test/tuf-conformance/env/pyvenv.cfg bin/tuf-conformance-entrypoint.xfails] 186 | end 187 | -------------------------------------------------------------------------------- /bin/conformance-entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] = File.expand_path("../Gemfile", __dir__) 5 | require "bundler/setup" 6 | 7 | require "tmpdir" 8 | 9 | tmp = Dir.mktmpdir 10 | 11 | ENV.update( 12 | "HOME" => tmp, 13 | "XDG_DATA_HOME" => nil, 14 | "XDG_CACHE_HOME" => nil 15 | ) 16 | 17 | require "sigstore/cli" 18 | Sigstore::CLI.start(ARGV << "--no-update-trusted-root") 19 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "sigstore" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rake", "rake") 28 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rubocop' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rubocop", "rubocop") 28 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/sigstore-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'sigstore-cli' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("sigstore-cli", "sigstore-cli") 28 | -------------------------------------------------------------------------------- /bin/smoketest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "fileutils" 5 | require "rake" 6 | require "net/http" 7 | require "json" 8 | 9 | include FileUtils # rubocop:disable Style/MixinUsage 10 | 11 | raise(StandardError, "Usage: #{$PROGRAM_NAME} ") if ARGV.empty? 12 | 13 | dists = ARGV 14 | mkdir_p %w[smoketest-gem-home smoketest-artifacts] 15 | 16 | at_exit { rm_rf "smoketest-gem-home" } 17 | 18 | env = { 19 | "PATH" => "smoketest-gem-home/bin:#{ENV.fetch("PATH")}", 20 | "GEM_HOME" => "smoketest-gem-home", 21 | "GEM_PATH" => "smoketest-gem-home", 22 | "BUNDLE_GEMFILE" => "smoketest-gem-home/Gemfile" 23 | } 24 | 25 | # Get cert identity from environment 26 | cert_identity = ENV.fetch("SIGSTORE_CERT_IDENTITY") 27 | 28 | # Read OIDC token from file if available 29 | oidc_token_file = ENV.fetch("OIDC_TOKEN_FILE", nil) 30 | oidc_token = (File.read(oidc_token_file).strip if oidc_token_file && File.exist?(oidc_token_file)) 31 | 32 | sh(env, "gem", "install", *dists, "--no-document", exception: true) 33 | 34 | File.write("smoketest-gem-home/Gemfile", <<~RUBY) 35 | gem "sigstore-cli" 36 | RUBY 37 | 38 | dists.each do |dist| 39 | # Build sign command with optional identity token 40 | sign_cmd = [ 41 | File.expand_path("sigstore-cli", __dir__), 42 | "sign", 43 | dist 44 | ] 45 | 46 | sign_cmd.push("--identity-token", oidc_token) if oidc_token 47 | 48 | sign_cmd.push( 49 | "--signature=smoketest-artifacts/#{File.basename(dist)}.sig", 50 | "--certificate=smoketest-artifacts/#{File.basename(dist)}.crt", 51 | "--bundle=smoketest-artifacts/#{File.basename(dist)}.sigstore.json" 52 | ) 53 | 54 | sh(env, *sign_cmd, exception: true) 55 | 56 | sh(env, File.expand_path("sigstore-cli", __dir__), 57 | "verify", 58 | "--signature=smoketest-artifacts/#{File.basename(dist)}.sig", 59 | "--certificate=smoketest-artifacts/#{File.basename(dist)}.crt", 60 | "--certificate-oidc-issuer=https://token.actions.githubusercontent.com", 61 | "--certificate-identity=#{cert_identity}", 62 | dist, 63 | exception: true) 64 | sh(env, File.expand_path("sigstore-cli", __dir__), 65 | "verify", 66 | "--bundle=smoketest-artifacts/#{File.basename(dist)}.sigstore.json", 67 | "--certificate-oidc-issuer=https://token.actions.githubusercontent.com", 68 | "--certificate-identity=#{cert_identity}", 69 | dist, 70 | exception: true) 71 | end 72 | -------------------------------------------------------------------------------- /bin/tuf-conformance-entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] = File.expand_path("../Gemfile", __dir__) 5 | require "bundler/setup" 6 | 7 | require "optparse" 8 | 9 | args = [] 10 | tmp = nil 11 | OptionParser.new do |parser| 12 | parser.on("--metadata-url U") do |v| 13 | args << "--metadata-url" << v 14 | end 15 | parser.on("--metadata-dir D") do |v| 16 | tmp = v 17 | args << "--metadata-dir" << v 18 | end 19 | parser.on("--targets-dir D") do |v| 20 | args << "--targets-dir" << v 21 | end 22 | parser.on("--cached") do |_v| 23 | args << "--cached" 24 | end 25 | parser.on("--target-base-url U") do |v| 26 | args << "--target-base-url" << v 27 | end 28 | parser.on("--target-name N") do |v| 29 | args << v 30 | end 31 | end.parse! 32 | 33 | ARGV.prepend("tuf") 34 | ARGV[2, 0] = args 35 | 36 | if ENV.fetch("FAKETIME", nil) && 37 | !ENV["DYLD_INSERT_LIBRARIES"].to_s.include?("libfaketime") && !ENV["LD_PRELOAD"].to_s.include?("libfaketime") 38 | Time.singleton_class.prepend(Module.new do 39 | def now 40 | super + ENV["FAKETIME"].to_f 41 | end 42 | end) 43 | end 44 | 45 | require "sigstore/cli" 46 | Sigstore::CLI.start(ARGV) 47 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | *.gem 3 | -------------------------------------------------------------------------------- /cli/exe/sigstore-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "sigstore/cli" 5 | Sigstore::CLI.start 6 | -------------------------------------------------------------------------------- /cli/lib/sigstore/cli/id_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Sigstore::CLI 4 | class IdToken 5 | include Sigstore::Loggable 6 | 7 | class AmbientCredentialError < Sigstore::Error 8 | end 9 | 10 | def self.detect_credential 11 | [ 12 | GitHub 13 | # detect_gcp, 14 | # detect_buildkite, 15 | # detect_gitlab, 16 | # detect_circleci 17 | ].each do |detector| 18 | credential = detector.call("sigstore") 19 | return credential if credential 20 | end 21 | 22 | logger.debug { "failed to find ambient OIDC credential" } 23 | 24 | nil 25 | end 26 | 27 | def self.call(audience) 28 | new(audience).call 29 | end 30 | 31 | def initialize(audience) 32 | @audience = audience 33 | end 34 | 35 | def call 36 | raise NotImplementedError, "#{self.class}#call" 37 | end 38 | 39 | class GitHub < IdToken 40 | class PermissionCredentialError < Sigstore::Error 41 | end 42 | 43 | def call 44 | logger.debug { "looking for OIDC credentials" } 45 | unless ENV["GITHUB_ACTIONS"] 46 | logger.debug { "environment doesn't look like a GH action; giving up" } 47 | return 48 | end 49 | 50 | req_token = ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_TOKEN", nil) 51 | unless req_token 52 | raise PermissionCredentialError, 53 | "missing or insufficient OIDC token permissions, " \ 54 | "the ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variable was unset" 55 | end 56 | 57 | req_url = ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_URL", nil) 58 | unless req_url 59 | raise PermissionCredentialError, 60 | "missing or insufficient OIDC token permissions, " \ 61 | "the ACTIONS_ID_TOKEN_REQUEST_URL environment variable was unset" 62 | end 63 | req_url = URI.parse(req_url) 64 | req_url.query = "audience=#{URI.encode_uri_component(@audience)}" 65 | 66 | logger.debug { "requesting OIDC token" } 67 | resp = Net::HTTP.get_response( 68 | req_url, { "Authorization" => "bearer #{req_token}" } 69 | ) 70 | 71 | begin 72 | resp.value 73 | rescue Net::HTTPExceptions 74 | raise AmbientCredentialError, "OIDC token request failed (code=#{resp.code}, body=#{resp.body})" 75 | rescue Timeout::Error 76 | raise AmbientCredentialError, "OIDC token request timed out" 77 | end 78 | 79 | begin 80 | body = JSON.parse resp.body 81 | rescue StandardError 82 | raise AmbientCredentialError, "malformed or incomplete json" 83 | else 84 | body.fetch("value") 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /cli/sigstore-cli.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../lib/sigstore/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "sigstore-cli" 7 | spec.version = Sigstore::VERSION 8 | spec.authors = ["The Sigstore Authors", "Samuel Giddins"] 9 | spec.email = [nil, "segiddins@segiddins.me"] 10 | 11 | spec.summary = "A CLI interface to the sigstore ruby client" 12 | spec.homepage = "https://github.com/sigstore/sigstore-ruby" 13 | spec.license = "Apache-2.0" 14 | spec.required_ruby_version = ">= 3.2.0" 15 | 16 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 17 | 18 | spec.metadata["homepage_uri"] = spec.homepage 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = IO.popen(["git", "-C", __dir__, "ls-files", "-z"], &:read).split("\x0").reject do |f| 23 | (File.expand_path(f) == __FILE__) || 24 | f.start_with?(*%w[bin/ test/ spec/ features/ fixtures/ . Rakefile Gemfile]) 25 | end 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "sigstore", spec.version 31 | spec.add_dependency "thor" 32 | 33 | spec.metadata["rubygems_mfa_required"] = "true" 34 | end 35 | -------------------------------------------------------------------------------- /data/_store/prod/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "6f260089d5923daf20166ca657c543af618346ab971884a99962b01988bbe0c3", 5 | "sig": "30460221008ab1f6f17d4f9e6d7dcf1c88912b6b53cc10388644ae1f09bc37a082cd06003e022100e145ef4c7b782d4e8107b53437e669d0476892ce999903ae33d14448366996e7" 6 | }, 7 | { 8 | "keyid": "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2", 9 | "sig": "3045022100c768b2f86da99569019c160a081da54ae36c34c0a3120d3cb69b53b7d113758e02204f671518f617b20d46537fae6c3b63bae8913f4f1962156105cc4f019ac35c6a" 10 | }, 11 | { 12 | "keyid": "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06", 13 | "sig": "3045022100b4434e6995d368d23e74759acd0cb9013c83a5d3511f0f997ec54c456ae4350a022015b0e265d182d2b61dc74e155d98b3c3fbe564ba05286aa14c8df02c9b756516" 14 | }, 15 | { 16 | "keyid": "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222", 17 | "sig": "304502210082c58411d989eb9f861410857d42381590ec9424dbdaa51e78ed13515431904e0220118185da6a6c2947131c17797e2bb7620ce26e5f301d1ceac5f2a7e58f9dcf2e" 18 | }, 19 | { 20 | "keyid": "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70", 21 | "sig": "3046022100c78513854cae9c32eaa6b88e18912f48006c2757a258f917312caba75948eb9e022100d9e1b4ce0adfe9fd2e2148d7fa27a2f40ba1122bd69da7612d8d1776b013c91d" 22 | }, 23 | { 24 | "keyid": "fdfa83a07b5a83589b87ded41f77f39d232ad91f7cce52868dacd06ba089849f", 25 | "sig": "3045022056483a2d5d9ea9cec6e11eadfb33c484b614298faca15acf1c431b11ed7f734c022100d0c1d726af92a87e4e66459ca5adf38a05b44e1f94318423f954bae8bca5bb2e" 26 | }, 27 | { 28 | "keyid": "e2f59acb9488519407e18cbfc9329510be03c04aca9929d2f0301343fec85523", 29 | "sig": "3046022100d004de88024c32dc5653a9f4843cfc5215427048ad9600d2cf9c969e6edff3d2022100d9ebb798f5fc66af10899dece014a8628ccf3c5402cd4a4270207472f8f6e712" 30 | }, 31 | { 32 | "keyid": "3c344aa068fd4cc4e87dc50b612c02431fbc771e95003993683a2b0bf260cf0e", 33 | "sig": "3046022100b7b09996c45ca2d4b05603e56baefa29718a0b71147cf8c6e66349baa61477df022100c4da80c717b4fa7bba0fd5c72da8a0499358b01358b2309f41d1456ea1e7e1d9" 34 | }, 35 | { 36 | "keyid": "ec81669734e017996c5b85f3d02c3de1dd4637a152019fe1af125d2f9368b95e", 37 | "sig": "3046022100be9782c30744e411a82fa85b5138d601ce148bc19258aec64e7ec24478f38812022100caef63dcaf1a4b9a500d3bd0e3f164ec18f1b63d7a9460d9acab1066db0f016d" 38 | }, 39 | { 40 | "keyid": "1e1d65ce98b10addad4764febf7dda2d0436b3d3a3893579c0dddaea20e54849", 41 | "sig": "30450220746ec3f8534ce55531d0d01ff64964ef440d1e7d2c4c142409b8e9769f1ada6f022100e3b929fcd93ea18feaa0825887a7210489879a66780c07a83f4bd46e2f09ab3b" 42 | } 43 | ], 44 | "signed": { 45 | "_type": "root", 46 | "consistent_snapshot": true, 47 | "expires": "2025-02-19T08:04:32Z", 48 | "keys": { 49 | "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06": { 50 | "keyid_hash_algorithms": [ 51 | "sha256", 52 | "sha512" 53 | ], 54 | "keytype": "ecdsa", 55 | "keyval": { 56 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\n-----END PUBLIC KEY-----\n" 57 | }, 58 | "scheme": "ecdsa-sha2-nistp256", 59 | "x-tuf-on-ci-keyowner": "@santiagotorres" 60 | }, 61 | "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222": { 62 | "keyid_hash_algorithms": [ 63 | "sha256", 64 | "sha512" 65 | ], 66 | "keytype": "ecdsa", 67 | "keyval": { 68 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\n-----END PUBLIC KEY-----\n" 69 | }, 70 | "scheme": "ecdsa-sha2-nistp256", 71 | "x-tuf-on-ci-keyowner": "@bobcallaway" 72 | }, 73 | "6f260089d5923daf20166ca657c543af618346ab971884a99962b01988bbe0c3": { 74 | "keyid_hash_algorithms": [ 75 | "sha256", 76 | "sha512" 77 | ], 78 | "keytype": "ecdsa", 79 | "keyval": { 80 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEy8XKsmhBYDI8Jc0GwzBxeKax0cm5\nSTKEU65HPFunUn41sT8pi0FjM4IkHz/YUmwmLUO0Wt7lxhj6BkLIK4qYAw==\n-----END PUBLIC KEY-----\n" 81 | }, 82 | "scheme": "ecdsa-sha2-nistp256", 83 | "x-tuf-on-ci-keyowner": "@dlorenc" 84 | }, 85 | "7247f0dbad85b147e1863bade761243cc785dcb7aa410e7105dd3d2b61a36d2c": { 86 | "keyid_hash_algorithms": [ 87 | "sha256", 88 | "sha512" 89 | ], 90 | "keytype": "ecdsa", 91 | "keyval": { 92 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\n-----END PUBLIC KEY-----\n" 93 | }, 94 | "scheme": "ecdsa-sha2-nistp256", 95 | "x-tuf-on-ci-online-uri": "gcpkms://projects/sigstore-root-signing/locations/global/keyRings/root/cryptoKeys/timestamp" 96 | }, 97 | "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70": { 98 | "keyid_hash_algorithms": [ 99 | "sha256", 100 | "sha512" 101 | ], 102 | "keytype": "ecdsa", 103 | "keyval": { 104 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\n-----END PUBLIC KEY-----\n" 105 | }, 106 | "scheme": "ecdsa-sha2-nistp256", 107 | "x-tuf-on-ci-keyowner": "@joshuagl" 108 | }, 109 | "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2": { 110 | "keyid_hash_algorithms": [ 111 | "sha256", 112 | "sha512" 113 | ], 114 | "keytype": "ecdsa", 115 | "keyval": { 116 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\n-----END PUBLIC KEY-----\n" 117 | }, 118 | "scheme": "ecdsa-sha2-nistp256", 119 | "x-tuf-on-ci-keyowner": "@mnm678" 120 | } 121 | }, 122 | "roles": { 123 | "root": { 124 | "keyids": [ 125 | "6f260089d5923daf20166ca657c543af618346ab971884a99962b01988bbe0c3", 126 | "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2", 127 | "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06", 128 | "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222", 129 | "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70" 130 | ], 131 | "threshold": 3 132 | }, 133 | "snapshot": { 134 | "keyids": [ 135 | "7247f0dbad85b147e1863bade761243cc785dcb7aa410e7105dd3d2b61a36d2c" 136 | ], 137 | "threshold": 1, 138 | "x-tuf-on-ci-expiry-period": 3650, 139 | "x-tuf-on-ci-signing-period": 365 140 | }, 141 | "targets": { 142 | "keyids": [ 143 | "6f260089d5923daf20166ca657c543af618346ab971884a99962b01988bbe0c3", 144 | "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2", 145 | "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06", 146 | "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222", 147 | "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70" 148 | ], 149 | "threshold": 3 150 | }, 151 | "timestamp": { 152 | "keyids": [ 153 | "7247f0dbad85b147e1863bade761243cc785dcb7aa410e7105dd3d2b61a36d2c" 154 | ], 155 | "threshold": 1, 156 | "x-tuf-on-ci-expiry-period": 7, 157 | "x-tuf-on-ci-signing-period": 4 158 | } 159 | }, 160 | "spec_version": "1.0", 161 | "version": 10, 162 | "x-tuf-on-ci-expiry-period": 182, 163 | "x-tuf-on-ci-signing-period": 31 164 | } 165 | } -------------------------------------------------------------------------------- /data/_store/prod/trusted_root.json: -------------------------------------------------------------------------------- 1 | { 2 | "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", 3 | "tlogs": [ 4 | { 5 | "baseUrl": "https://rekor.sigstore.dev", 6 | "hashAlgorithm": "SHA2_256", 7 | "publicKey": { 8 | "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", 9 | "keyDetails": "PKIX_ECDSA_P256_SHA_256", 10 | "validFor": { 11 | "start": "2021-01-12T11:53:27.000Z" 12 | } 13 | }, 14 | "logId": { 15 | "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" 16 | } 17 | } 18 | ], 19 | "certificateAuthorities": [ 20 | { 21 | "subject": { 22 | "organization": "sigstore.dev", 23 | "commonName": "sigstore" 24 | }, 25 | "uri": "https://fulcio.sigstore.dev", 26 | "certChain": { 27 | "certificates": [ 28 | { 29 | "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" 30 | } 31 | ] 32 | }, 33 | "validFor": { 34 | "start": "2021-03-07T03:20:29.000Z", 35 | "end": "2022-12-31T23:59:59.999Z" 36 | } 37 | }, 38 | { 39 | "subject": { 40 | "organization": "sigstore.dev", 41 | "commonName": "sigstore" 42 | }, 43 | "uri": "https://fulcio.sigstore.dev", 44 | "certChain": { 45 | "certificates": [ 46 | { 47 | "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" 48 | }, 49 | { 50 | "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" 51 | } 52 | ] 53 | }, 54 | "validFor": { 55 | "start": "2022-04-13T20:06:15.000Z" 56 | } 57 | } 58 | ], 59 | "ctlogs": [ 60 | { 61 | "baseUrl": "https://ctfe.sigstore.dev/test", 62 | "hashAlgorithm": "SHA2_256", 63 | "publicKey": { 64 | "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", 65 | "keyDetails": "PKIX_ECDSA_P256_SHA_256", 66 | "validFor": { 67 | "start": "2021-03-14T00:00:00.000Z", 68 | "end": "2022-10-31T23:59:59.999Z" 69 | } 70 | }, 71 | "logId": { 72 | "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" 73 | } 74 | }, 75 | { 76 | "baseUrl": "https://ctfe.sigstore.dev/2022", 77 | "hashAlgorithm": "SHA2_256", 78 | "publicKey": { 79 | "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", 80 | "keyDetails": "PKIX_ECDSA_P256_SHA_256", 81 | "validFor": { 82 | "start": "2022-10-20T00:00:00.000Z" 83 | } 84 | }, 85 | "logId": { 86 | "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" 87 | } 88 | } 89 | ], 90 | "timestampAuthorities": [ 91 | { 92 | "subject": { 93 | "organization": "GitHub, Inc.", 94 | "commonName": "Internal Services Root" 95 | }, 96 | "certChain": { 97 | "certificates": [ 98 | { 99 | "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" 100 | }, 101 | { 102 | "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" 103 | }, 104 | { 105 | "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" 106 | } 107 | ] 108 | }, 109 | "validFor": { 110 | "start": "2023-04-14T00:00:00.000Z" 111 | } 112 | } 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /data/_store/staging/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81", 5 | "sig": "304502204d5d01c2ae4b846cc6d29d7c5676f5d99ea464a69bd464fef16a5d0cdd4a616d022100bf73b2b11b68bf7a7047480bf0d5961a3a40c524f64a82e2c90f59d4083e498e" 6 | }, 7 | { 8 | "keyid": "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc", 9 | "sig": "3044022005a8e904d484b7f4c3bac53ed6babeee303f6308f81f9ea29a7a1f6ad51068c20220641303f1e5ab14b151525c63ca95b35df64ffc905c8883f96cbee703ed45a2df" 10 | }, 11 | { 12 | "keyid": "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237", 13 | "sig": "" 14 | }, 15 | { 16 | "keyid": "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5", 17 | "sig": "" 18 | } 19 | ], 20 | "signed": { 21 | "_type": "root", 22 | "consistent_snapshot": true, 23 | "expires": "2025-03-07T07:44:40Z", 24 | "keys": { 25 | "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5": { 26 | "keytype": "ecdsa", 27 | "keyval": { 28 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoxkvDOmtGEknB3M+ZkPts8joDM0X\nIH5JZwPlgC2CXs/eqOuNF8AcEWwGYRiDhV/IMlQw5bg8PLICQcgsbrDiKg==\n-----END PUBLIC KEY-----\n" 29 | }, 30 | "scheme": "ecdsa-sha2-nistp256", 31 | "x-tuf-on-ci-keyowner": "@mnm678" 32 | }, 33 | "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc": { 34 | "keytype": "ecdsa", 35 | "keyval": { 36 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE++Wv+DcLRk+mfkmlpCwl1GUi9EMh\npBUTz8K0fH7bE4mQuViGSyWA/eyMc0HvzZi6Xr0diHw0/lUPBvok214YQw==\n-----END PUBLIC KEY-----\n" 37 | }, 38 | "scheme": "ecdsa-sha2-nistp256", 39 | "x-tuf-on-ci-keyowner": "@kommendorkapten" 40 | }, 41 | "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237": { 42 | "keytype": "ecdsa", 43 | "keyval": { 44 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFHDb85JH+JYR1LQmxiz4UMokVMnP\nxKoWpaEnFCKXH8W4Fc/DfIxMnkpjCuvWUBdJXkO0aDIxwsij8TOFh2R7dw==\n-----END PUBLIC KEY-----\n" 45 | }, 46 | "scheme": "ecdsa-sha2-nistp256", 47 | "x-tuf-on-ci-keyowner": "@joshuagl" 48 | }, 49 | "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81": { 50 | "keytype": "ecdsa", 51 | "keyval": { 52 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEohqIdE+yTl4OxpX8ZxNUPrg3SL9H\nBDnhZuceKkxy2oMhUOxhWweZeG3bfM1T4ZLnJimC6CAYVU5+F5jZCoftRw==\n-----END PUBLIC KEY-----\n" 53 | }, 54 | "scheme": "ecdsa-sha2-nistp256", 55 | "x-tuf-on-ci-keyowner": "@jku" 56 | }, 57 | "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4": { 58 | "keytype": "ecdsa", 59 | "keyval": { 60 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExxmEtmhF5U+i+v/6he4BcSLzCgMx\n/0qSrvDg6bUWwUrkSKS2vDpcJrhGy5fmmhRrGawjPp1ALpC3y1kqFTpXDg==\n-----END PUBLIC KEY-----\n" 61 | }, 62 | "scheme": "ecdsa-sha2-nistp256", 63 | "x-tuf-on-ci-online-uri": "gcpkms:projects/projectsigstore-staging/locations/global/keyRings/tuf-keyring/cryptoKeys/tuf-key/cryptoKeyVersions/2" 64 | } 65 | }, 66 | "roles": { 67 | "root": { 68 | "keyids": [ 69 | "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81", 70 | "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc", 71 | "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237", 72 | "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5" 73 | ], 74 | "threshold": 2 75 | }, 76 | "snapshot": { 77 | "keyids": [ 78 | "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4" 79 | ], 80 | "threshold": 1, 81 | "x-tuf-on-ci-expiry-period": 3650, 82 | "x-tuf-on-ci-signing-period": 365 83 | }, 84 | "targets": { 85 | "keyids": [ 86 | "aa61e09f6af7662ac686cf0c6364079f63d3e7a86836684eeced93eace3acd81", 87 | "61f9609d2655b346fcebccd66b509d5828168d5e447110e261f0bcc8553624bc", 88 | "9471fbda95411d10109e467ad526082d15f14a38de54ea2ada9687ab39d8e237", 89 | "0374a9e18a20a2103736cb4277e2fdd7f8453642c7d9eaf4ad8aee9cf2d47bb5" 90 | ], 91 | "threshold": 1 92 | }, 93 | "timestamp": { 94 | "keyids": [ 95 | "c3479007e861445ce5dc109d9661ed77b35bbc0e3f161852c46114266fc2daa4" 96 | ], 97 | "threshold": 1, 98 | "x-tuf-on-ci-expiry-period": 7, 99 | "x-tuf-on-ci-signing-period": 6 100 | } 101 | }, 102 | "spec_version": "1.0", 103 | "version": 10, 104 | "x-tuf-on-ci-expiry-period": 182, 105 | "x-tuf-on-ci-signing-period": 35 106 | } 107 | } -------------------------------------------------------------------------------- /data/_store/staging/trusted_root.json: -------------------------------------------------------------------------------- 1 | { 2 | "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", 3 | "tlogs": [ 4 | { 5 | "baseUrl": "https://rekor.sigstage.dev", 6 | "hashAlgorithm": "SHA2_256", 7 | "publicKey": { 8 | "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", 9 | "keyDetails": "PKIX_ECDSA_P256_SHA_256", 10 | "validFor": { 11 | "start": "2021-01-12T11:53:27.000Z" 12 | } 13 | }, 14 | "logId": { 15 | "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" 16 | } 17 | } 18 | ], 19 | "certificateAuthorities": [ 20 | { 21 | "subject": { 22 | "organization": "sigstore.dev", 23 | "commonName": "sigstore" 24 | }, 25 | "uri": "https://fulcio.sigstage.dev", 26 | "certChain": { 27 | "certificates": [ 28 | { 29 | "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" 30 | }, 31 | { 32 | "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" 33 | } 34 | ] 35 | }, 36 | "validFor": { 37 | "start": "2022-04-14T21:38:40.000Z" 38 | } 39 | } 40 | ], 41 | "ctlogs": [ 42 | { 43 | "baseUrl": "https://ctfe.sigstage.dev/test", 44 | "hashAlgorithm": "SHA2_256", 45 | "publicKey": { 46 | "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", 47 | "keyDetails": "PKCS1_RSA_PKCS1V5", 48 | "validFor": { 49 | "start": "2021-03-14T00:00:00.000Z", 50 | "end": "2022-07-31T00:00:00.000Z" 51 | } 52 | }, 53 | "logId": { 54 | "keyId": "G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=" 55 | } 56 | }, 57 | { 58 | "baseUrl": "https://ctfe.sigstage.dev/2022", 59 | "hashAlgorithm": "SHA2_256", 60 | "publicKey": { 61 | "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", 62 | "keyDetails": "PKIX_ECDSA_P256_SHA_256", 63 | "validFor": { 64 | "start": "2022-07-01T00:00:00.000Z", 65 | "end": "2022-07-31T00:00:00.000Z" 66 | } 67 | }, 68 | "logId": { 69 | "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" 70 | } 71 | }, 72 | { 73 | "baseUrl": "https://ctfe.sigstage.dev/2022-2", 74 | "hashAlgorithm": "SHA2_256", 75 | "publicKey": { 76 | "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", 77 | "keyDetails": "PKIX_ECDSA_P256_SHA_256", 78 | "validFor": { 79 | "start": "2022-07-01T00:00:00.000Z" 80 | } 81 | }, 82 | "logId": { 83 | "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" 84 | } 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /lib/sigstore.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | class << self 19 | attr_writer :logger 20 | 21 | def logger 22 | @logger ||= begin 23 | require "logger" 24 | Logger.new($stderr) 25 | end 26 | end 27 | end 28 | 29 | module Loggable 30 | def logger 31 | self.class.logger 32 | end 33 | 34 | def self.included(base) 35 | base.extend(ClassMethods) 36 | end 37 | 38 | module ClassMethods 39 | def logger 40 | Sigstore.logger 41 | end 42 | end 43 | end 44 | end 45 | 46 | require_relative "sigstore/verifier" 47 | require_relative "sigstore/signer" 48 | -------------------------------------------------------------------------------- /lib/sigstore/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | class Error < StandardError 19 | class InvalidSignature < Error; end 20 | class InvalidBundle < Error; end 21 | class InvalidCertificate < Error; end 22 | class NoCertificate < Error; end 23 | class NoTrustedRoot < Error; end 24 | class NoBundle < Error; end 25 | class NoSignature < Error; end 26 | class InvalidKey < Error; end 27 | class InvalidCheckpoint < Error; end 28 | class InvalidVerificationInput < Error; end 29 | 30 | class Signing < Error; end 31 | class InvalidIdentityToken < Error; end 32 | 33 | class MissingRekorEntry < Error; end 34 | class InvalidRekorEntry < Error; end 35 | class FailedRekorLookup < Error; end 36 | class FailedRekorPost < Error; end 37 | 38 | class Unimplemented < Error; end 39 | 40 | class UnsupportedPlatform < Error; end 41 | class UnsupportedKeyType < Error; end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/sigstore/internal/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore::Internal 18 | module JSON 19 | # Implements https://wiki.laptop.org/go/Canonical_JSON 20 | # 21 | def self.canonical_generate(data, buffer = +"") 22 | case data 23 | when NilClass 24 | buffer << "null" 25 | when TrueClass 26 | buffer << "true" 27 | when FalseClass 28 | buffer << "false" 29 | when Integer 30 | buffer << data.to_s 31 | when String 32 | buffer << '"' << data.gsub(/(["\\])/, '\\\\\1') << '"' 33 | when Array 34 | buffer << "[" 35 | data.each_with_index do |v, i| 36 | buffer << "," unless i.zero? 37 | canonical_generate(v, buffer) 38 | end 39 | buffer << "]" 40 | when Hash 41 | contents = data.sort_by do |k, _| 42 | raise ArgumentError, "Non-string key in hash" unless k.is_a?(String) 43 | 44 | k.encode("utf-16").codepoints 45 | end 46 | buffer << "{" 47 | comma = false 48 | contents.each do |k, v| 49 | if comma 50 | buffer << "," 51 | else 52 | comma = true 53 | end 54 | canonical_generate(k, buffer) 55 | buffer << ":" 56 | canonical_generate(v, buffer) 57 | end 58 | buffer << "}" 59 | else 60 | raise ArgumentError, "Unsupported data type: #{data.class}" 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/sigstore/internal/key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "util" 18 | 19 | module Sigstore 20 | module Internal 21 | class Key 22 | include Loggable 23 | 24 | def self.from_key_details(key_details, key_bytes) 25 | case key_details 26 | when Common::V1::PublicKeyDetails::PKIX_ECDSA_P256_SHA_256 27 | key_type = "ecdsa" 28 | key_schema = "ecdsa-sha2-nistp256" 29 | when Common::V1::PublicKeyDetails::PKCS1_RSA_PKCS1V5 30 | key_type = "rsa" 31 | key_schema = "rsa-pkcs1v15-sha256" 32 | else 33 | # Skip unrecognized key types instead of raising an error. 34 | # This allows the library to work with newer trusted roots that include 35 | # key types we don't yet support (e.g., PKIX_ED25519 for Rekor v2). 36 | logger.warn { "Skipping unrecognized key type: #{key_details}" } 37 | return nil 38 | end 39 | 40 | read(key_type, key_schema, key_bytes, key_id: OpenSSL::Digest::SHA256.hexdigest(key_bytes)) 41 | end 42 | 43 | def self.read(key_type, schema, key_bytes, key_id: nil) 44 | case key_type 45 | when "ecdsa", "ecdsa-sha2-nistp256" 46 | pkey = OpenSSL::PKey::EC.new(key_bytes) 47 | EDCSA.new(key_type, schema, pkey, key_id:) 48 | when "ed25519" 49 | pkey = ED25519.pkey_from_der([key_bytes].pack("H*")) 50 | ED25519.new(key_type, schema, pkey, key_id:) 51 | when "rsa" 52 | pkey = OpenSSL::PKey::RSA.new(key_bytes) 53 | RSA.new(key_type, schema, pkey, key_id:) 54 | else 55 | raise ArgumentError, "Unsupported key type #{key_type}" 56 | end 57 | rescue OpenSSL::PKey::PKeyError => e 58 | raise OpenSSL::PKey::PKeyError, "Invalid key: #{e} for #{key_type} #{schema} #{key_id}" 59 | end 60 | 61 | attr_reader :key_type, :schema, :key_id 62 | 63 | def initialize(key_type, schema, key, key_id: nil) 64 | @key_type = key_type 65 | @key = key 66 | @schema = schema 67 | @key_id = key_id 68 | end 69 | 70 | def to_pem 71 | @key.to_pem 72 | end 73 | 74 | def to_der 75 | @key.to_der 76 | end 77 | 78 | def verify(algo, signature, data) 79 | @key.verify(algo, signature, data) 80 | rescue OpenSSL::PKey::PKeyError => e 81 | logger.debug { "Verification failed: #{e}" } 82 | false 83 | end 84 | 85 | def public_to_der 86 | @key.public_to_der 87 | end 88 | 89 | class EDCSA < Key 90 | def initialize(...) 91 | super 92 | unless @key_type == "ecdsa" || @key_type == "ecdsa-sha2-nistp256" 93 | raise ArgumentError, 94 | "key_type must be edcsa, given #{@key_type}" 95 | end 96 | unless @key.is_a?(OpenSSL::PKey::EC) 97 | raise ArgumentError, 98 | "key must be an OpenSSL::PKey::EC, is #{@key.inspect}" 99 | end 100 | 101 | case @schema 102 | when "ecdsa-sha2-nistp256" 103 | unless @key.group.curve_name == "prime256v1" 104 | raise ArgumentError, "Expected prime256v1 curve, got #{@key.group.curve_name}" 105 | end 106 | else 107 | raise ArgumentError, "Unsupported schema #{schema}" 108 | end 109 | end 110 | end 111 | 112 | class RSA < Key 113 | def initialize(...) 114 | super 115 | raise ArgumentError, "key_type must be rsa, given #{@key_type}" unless @key_type == "rsa" 116 | 117 | unless @key.is_a?(OpenSSL::PKey::RSA) 118 | raise ArgumentError, "key must be an OpenSSL::PKey::RSA, given #{@key.inspect}" 119 | end 120 | 121 | case @schema 122 | when "rsassa-pss-sha256" 123 | raise Error::UnsupportedPlatform, "RSA-PSS verification unsupported" unless @key.respond_to?(:verify_pss) 124 | when "rsa-pkcs1v15-sha256" 125 | # supported 126 | else 127 | raise ArgumentError, "Unsupported schema #{schema}" 128 | end 129 | end 130 | 131 | def verify(_algo, signature, data) 132 | case @schema 133 | when "rsassa-pss-sha256" 134 | @key.verify_pss("sha256", signature, data, salt_length: :auto, mgf1_hash: "SHA256") 135 | when "rsa-pkcs1v15-sha256" 136 | super 137 | else 138 | raise ArgumentError, "Unsupported schema #{schema}" 139 | end 140 | end 141 | end 142 | 143 | class ED25519 < Key 144 | def self.pkey_from_der(raw) 145 | if OpenSSL::PKey.respond_to?(:new_raw_public_key) 146 | OpenSSL::PKey.new_raw_public_key("ed25519", raw) 147 | else 148 | pem = <<~PEM 149 | -----BEGIN PUBLIC KEY----- 150 | MCowBQYDK2VwAyEA#{Internal::Util.base64_encode(raw)} 151 | -----END PUBLIC KEY----- 152 | PEM 153 | OpenSSL::PKey.read(pem) 154 | end 155 | end 156 | 157 | def initialize(...) 158 | super 159 | unless @key_type == "ed25519" 160 | raise ArgumentError, 161 | "key_type must be ed25519, given #{@key_type}" 162 | end 163 | unless @key.is_a?(OpenSSL::PKey::PKey) && @key.oid == "ED25519" 164 | raise ArgumentError, 165 | "key must be an OpenSSL::PKey::PKey with oid ED25519, is #{@key.inspect}" 166 | end 167 | raise ArgumentError, "schema must be #{schema}" unless @schema == schema 168 | 169 | case @schema 170 | when "ed25519" 171 | # supported 172 | else 173 | raise ArgumentError, "Unsupported schema #{schema}" 174 | end 175 | end 176 | 177 | def verify(_algo, signature, data) 178 | super(nil, signature, data) 179 | end 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/sigstore/internal/keyring.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | module Internal 19 | class Keyring 20 | def initialize(keys:) 21 | @keyring = {} 22 | keys.each do |key| 23 | raise Error, "Duplicate key id #{key.key_id} in keyring" if @keyring.key?(key.key_id) 24 | 25 | @keyring[key.key_id] = key 26 | end 27 | end 28 | 29 | def verify(key_id:, signature:, data:) 30 | key = @keyring.fetch(key_id) { raise KeyError, "key not found: #{key_id.inspect}, known: #{@keyring.keys}" } 31 | 32 | return true if key.verify("SHA256", signature, data) 33 | 34 | raise(Error::InvalidSignature, 35 | "invalid signature: #{signature.inspect} over #{data.inspect} with key #{key_id.inspect}") 36 | rescue OpenSSL::PKey::PKeyError => e 37 | raise(Error::InvalidSignature, 38 | "#{e}: invalid signature: #{signature.inspect} over #{data.inspect} with key #{key_id.inspect}") 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/sigstore/internal/merkle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "util" 18 | 19 | module Sigstore 20 | module Internal 21 | module Merkle 22 | class MissingInclusionProofError < StandardError; end 23 | class MissingHashError < StandardError; end 24 | class InvalidInclusionProofError < StandardError; end 25 | class InclusionProofSizeError < InvalidInclusionProofError; end 26 | 27 | def self.verify_merkle_inclusion(entry) 28 | inclusion_proof = entry.inclusion_proof 29 | raise MissingInclusionProofError, "Rekor entry has no inclusion proof" unless inclusion_proof 30 | 31 | leaf_hash = hash_leaf(entry.canonicalized_body) 32 | verify_inclusion(inclusion_proof.log_index, inclusion_proof.tree_size, 33 | inclusion_proof.hashes, 34 | inclusion_proof.root_hash, leaf_hash) 35 | end 36 | 37 | def self.verify_inclusion(index, tree_size, proof, root, leaf_hash) 38 | calc_hash = root_from_inclusion_proof(index, tree_size, proof, leaf_hash) 39 | 40 | return if calc_hash == root 41 | 42 | raise InvalidInclusionProofError, 43 | "Inclusion proof contains invalid root hash: " \ 44 | "expected #{root.unpack1("H*")}, calculated #{calc_hash.unpack1("H*")}" 45 | end 46 | 47 | def self.root_from_inclusion_proof(log_index, tree_size, proof, leaf_hash) 48 | if log_index >= tree_size 49 | raise InclusionProofSizeError, 50 | "Log index #{log_index} is greater than tree size #{tree_size}" 51 | end 52 | 53 | if leaf_hash.bytesize != 32 54 | raise InvalidInclusionProofError, 55 | "Leaf hash has wrong size, expected 32 bytes, got #{leaf_hash.size}" 56 | end 57 | 58 | if proof.any? { |i| i.bytesize != 32 } 59 | raise InvalidInclusionProofError, 60 | "Proof hashes have wrong sizes, expected 32 bytes, got #{proof.inspect}" 61 | end 62 | 63 | inner, border = decompose_inclusion_proof(log_index, tree_size) 64 | 65 | if proof.size != inner + border 66 | raise InclusionProofSizeError, 67 | "Inclusion proof has wrong size, expected #{inner + border} hashes, got #{proof.size}" 68 | end 69 | 70 | intermediate_result = chain_inner( 71 | leaf_hash, 72 | (proof[...inner] || raise(MissingHashError, "missing left hashes")), 73 | log_index 74 | ) 75 | 76 | chain_border_right( 77 | intermediate_result, 78 | proof[inner..] || raise(MissingHashError, "missing right hashes") 79 | ) 80 | end 81 | 82 | def self.decompose_inclusion_proof(log_index, tree_size) 83 | inner = (log_index ^ (tree_size - 1)).bit_length 84 | border = (log_index >> inner).to_s(2).count("1") 85 | 86 | [inner, border] 87 | end 88 | 89 | def self.hash_leaf(data) 90 | data = "\u0000#{data}".b 91 | OpenSSL::Digest.new("SHA256").digest(data) 92 | end 93 | 94 | def self.chain_inner(seed, hashes, log_index) 95 | hashes.each_with_index do |hash, i| 96 | seed = if ((log_index >> i) & 1).zero? 97 | hash_children(seed, hash) 98 | else 99 | hash_children(hash, seed) 100 | end 101 | end 102 | seed 103 | end 104 | 105 | def self.chain_border_right(seed, hashes) 106 | hashes.reduce(seed) do |acc, hash| 107 | hash_children(hash, acc) 108 | end 109 | end 110 | 111 | def self.hash_children(left, right) 112 | data = "\u0001#{left}#{right}".b 113 | OpenSSL::Digest.new("SHA256").digest(data) 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/sigstore/internal/set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | module Internal 19 | module SET 20 | def self.verify_set(keyring:, entry:) 21 | raise Error, "invalid log entry: no inclusion promise" unless entry.inclusion_promise 22 | 23 | signed_entry_timestamp = entry.inclusion_promise.signed_entry_timestamp 24 | log_id = Util.hex_encode(entry.log_id.key_id) 25 | 26 | # https://www.rfc-editor.org/rfc/rfc8785 27 | canonical_entry = ::JSON.dump({ 28 | body: Internal::Util.base64_encode(entry.canonicalized_body), 29 | integratedTime: entry.integrated_time, 30 | logID: log_id, 31 | logIndex: entry.log_index 32 | }) 33 | 34 | keyring.verify( 35 | key_id: log_id, 36 | signature: signed_entry_timestamp, 37 | data: canonical_entry 38 | ) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/sigstore/internal/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | module Internal 19 | module Util 20 | module_function 21 | 22 | def hash_algorithm_name(algorithm) 23 | case algorithm 24 | when Common::V1::HashAlgorithm::SHA2_256 25 | "sha256" 26 | when Common::V1::HashAlgorithm::SHA2_384 27 | "sha384" 28 | when Common::V1::HashAlgorithm::SHA2_512 29 | "sha512" 30 | else 31 | raise ArgumentError, "Unrecognized hash algorithm #{algorithm}" 32 | end 33 | end 34 | 35 | def hex_encode(string) 36 | string.unpack1("H*") 37 | end 38 | 39 | def hex_decode(string) 40 | [string].pack("H*") 41 | end 42 | 43 | def base64_encode(string) 44 | [string].pack("m0") 45 | end 46 | 47 | def base64_decode(string) 48 | string.unpack1("m0") 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/sigstore/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "error" 18 | 19 | require_relative "trusted_root" 20 | 21 | module Sigstore 22 | VerificationResult = Struct.new(:success, keyword_init: true) do 23 | # @implements VerificationResult 24 | 25 | alias_method :verified?, :success 26 | end 27 | 28 | class VerificationSuccess < VerificationResult 29 | # @implements VerificationSuccess 30 | def initialize 31 | super(success: true) 32 | end 33 | end 34 | 35 | class VerificationFailure < VerificationResult 36 | # @implements VerificationFailure 37 | attr_reader :reason 38 | 39 | def initialize(reason) 40 | @reason = reason 41 | super(success: false) 42 | end 43 | end 44 | 45 | class BundleType 46 | include Comparable 47 | 48 | attr_reader :media_type 49 | 50 | def initialize(media_type) 51 | @media_type = media_type 52 | end 53 | 54 | BUNDLE_0_1 = new("application/vnd.dev.sigstore.bundle+json;version=0.1") 55 | BUNDLE_0_2 = new("application/vnd.dev.sigstore.bundle+json;version=0.2") 56 | BUNDLE_0_3 = new("application/vnd.dev.sigstore.bundle.v0.3+json") 57 | 58 | VERSIONS = [BUNDLE_0_1, BUNDLE_0_2, BUNDLE_0_3].freeze 59 | 60 | def self.from_media_type(media_type) 61 | case media_type 62 | when BUNDLE_0_1.media_type 63 | BUNDLE_0_1 64 | when BUNDLE_0_2.media_type 65 | BUNDLE_0_2 66 | when BUNDLE_0_3.media_type, "application/vnd.dev.sigstore.bundle+json;version=0.3" 67 | BUNDLE_0_3 68 | else 69 | raise Error::InvalidBundle, "Unsupported bundle format: #{media_type.inspect}" 70 | end 71 | end 72 | 73 | def <=>(other) 74 | VERSIONS.index(self) <=> VERSIONS.index(other) 75 | end 76 | end 77 | 78 | class VerificationInput < DelegateClass(Verification::V1::Input) 79 | attr_reader :trusted_root, :sbundle, :hashed_input 80 | 81 | def initialize(*) 82 | super 83 | 84 | unless bundle.is_a?(Bundle::V1::Bundle) 85 | raise ArgumentError, 86 | "bundle must be a #{Bundle::V1::Bundle}, is #{bundle.class}" 87 | end 88 | 89 | @trusted_root = TrustedRoot.new(artifact_trust_root) 90 | @sbundle = SBundle.new(bundle) 91 | if sbundle.message_signature? && !artifact 92 | raise Error::InvalidVerificationInput, "bundle with message_signature requires an artifact" 93 | end 94 | 95 | case artifact.data 96 | when :artifact_uri 97 | unless artifact.artifact_uri.start_with?("sha256:") 98 | raise Error::InvalidVerificationInput, 99 | "artifact_uri must be prefixed with 'sha256:'" 100 | end 101 | 102 | @hashed_input = Common::V1::HashOutput.new.tap do |hash_output| 103 | hash_output.algorithm = Common::V1::HashAlgorithm::SHA2_256 104 | hexdigest = artifact.artifact_uri.split(":", 2).last 105 | hash_output.digest = Internal::Util.hex_decode(hexdigest) 106 | end 107 | when :artifact 108 | @hashed_input = Common::V1::HashOutput.new.tap do |hash_output| 109 | hash_output.algorithm = Common::V1::HashAlgorithm::SHA2_256 110 | hash_output.digest = OpenSSL::Digest.new("SHA256").update(artifact.artifact).digest 111 | end 112 | else 113 | raise Error::InvalidVerificationInput, "Unsupported artifact data: #{artifact.data}" 114 | end 115 | 116 | freeze 117 | end 118 | end 119 | 120 | class SBundle < DelegateClass(Bundle::V1::Bundle) 121 | attr_reader :bundle_type, :leaf_certificate 122 | 123 | def initialize(*) 124 | super 125 | @bundle_type = BundleType.from_media_type(media_type) 126 | validate_version! 127 | freeze 128 | end 129 | 130 | def self.for_cert_bytes_and_signature(cert_bytes, signature) 131 | bundle = Bundle::V1::Bundle.new 132 | bundle.media_type = BundleType::BUNDLE_0_3.media_type 133 | bundle.verification_material = Bundle::V1::VerificationMaterial.new 134 | bundle.verification_material.certificate = Common::V1::X509Certificate.new 135 | bundle.verification_material.certificate.raw_bytes = cert_bytes 136 | bundle.message_signature = Common::V1::MessageSignature.new 137 | bundle.message_signature.signature = signature 138 | new(bundle) 139 | end 140 | 141 | def expected_tlog_entry(hashed_input) 142 | case content 143 | when :message_signature 144 | expected_hashed_rekord_tlog_entry(hashed_input) 145 | when :dsse_envelope 146 | rekor_entry = verification_material.tlog_entries.first 147 | canonicalized_body = begin 148 | JSON.parse(rekor_entry.canonicalized_body) 149 | rescue JSON::ParserError 150 | raise Error::InvalidBundle, "expected canonicalized_body to be JSON" 151 | end 152 | 153 | case kind_version = canonicalized_body.values_at("kind", "apiVersion") 154 | when %w[dsse 0.0.1] 155 | expected_dsse_0_0_1_tlog_entry 156 | when %w[intoto 0.0.2] 157 | expected_intoto_0_0_2_tlog_entry 158 | else 159 | raise Error::InvalidRekorEntry, "Unhandled rekor entry kind/version: #{kind_version.inspect}" 160 | end 161 | else 162 | raise Error::InvalidBundle, "expected either message_signature or dsse_envelope" 163 | end 164 | end 165 | 166 | private 167 | 168 | def validate_version! 169 | raise Error::InvalidBundle, "bundle requires verification material" unless verification_material 170 | 171 | case bundle_type 172 | when BundleType::BUNDLE_0_1 173 | unless verification_material.tlog_entries.all?(&:inclusion_promise) 174 | raise Error::InvalidBundle, 175 | "bundle v0.1 requires an inclusion promise" 176 | end 177 | if verification_material.tlog_entries.any? { |t| t.inclusion_proof&.checkpoint.nil? } 178 | raise Error::InvalidBundle, 179 | "0.1 bundle contains an inclusion proof without checkpoint" 180 | end 181 | else 182 | unless verification_material.tlog_entries.all?(&:inclusion_proof) 183 | raise Error::InvalidBundle, 184 | "must contain an inclusion proof" 185 | end 186 | unless verification_material.tlog_entries.all? { |t| t.inclusion_proof.checkpoint&.envelope } 187 | raise Error::InvalidBundle, 188 | "inclusion proof must contain a checkpoint" 189 | end 190 | end 191 | 192 | raise Error::InvalidBundle, "Expected one tlog entry" if verification_material.tlog_entries.size > 1 193 | 194 | case verification_material.content 195 | when :public_key 196 | raise Error::Unimplemented, "public_key content of bundle" 197 | when :x509_certificate_chain 198 | certs = verification_material.x509_certificate_chain.certificates.map do |cert| 199 | Internal::X509::Certificate.read(cert.raw_bytes) 200 | end 201 | 202 | @leaf_certificate = certs.first 203 | certs.each do |cert| 204 | raise Error::InvalidBundle, "Root CA in chain" if cert.ca? 205 | end 206 | when :certificate 207 | @leaf_certificate = Internal::X509::Certificate.read(verification_material.certificate.raw_bytes) 208 | else 209 | raise Error::InvalidBundle, "Unsupported bundle content: #{content.inspect}" 210 | end 211 | raise Error::InvalidBundle, "expected certificate to be leaf" unless @leaf_certificate.leaf? 212 | end 213 | 214 | def expected_hashed_rekord_tlog_entry(hashed_input) 215 | { 216 | "spec" => { 217 | "signature" => { 218 | "content" => Internal::Util.base64_encode(message_signature.signature), 219 | "publicKey" => { 220 | "content" => Internal::Util.base64_encode(leaf_certificate.to_pem) 221 | } 222 | }, 223 | "data" => { 224 | "hash" => { 225 | "algorithm" => Internal::Util.hash_algorithm_name(hashed_input.algorithm), 226 | "value" => Internal::Util.hex_encode(hashed_input.digest) 227 | } 228 | } 229 | }, 230 | "kind" => "hashedrekord", 231 | "apiVersion" => "0.0.1" 232 | } 233 | end 234 | 235 | def expected_intoto_0_0_2_tlog_entry 236 | { 237 | "apiVersion" => "0.0.2", 238 | "kind" => "intoto", 239 | "spec" => { 240 | "content" => { 241 | "envelope" => { 242 | "payloadType" => dsse_envelope.payloadType, 243 | "payload" => Internal::Util.base64_encode(Internal::Util.base64_encode(dsse_envelope.payload)), 244 | "signatures" => dsse_envelope.signatures.map do |sig| 245 | { 246 | "publicKey" => 247 | # needed because #to_pem packs the key in base64 with m* 248 | Internal::Util.base64_encode( 249 | "-----BEGIN CERTIFICATE-----\n" \ 250 | "#{Internal::Util.base64_encode(leaf_certificate.to_der)}\n" \ 251 | "-----END CERTIFICATE-----\n" 252 | ), 253 | "sig" => Internal::Util.base64_encode(Internal::Util.base64_encode(sig.sig)) 254 | } 255 | end 256 | }, 257 | "payloadHash" => { 258 | "algorithm" => "sha256", 259 | "value" => OpenSSL::Digest::SHA256.hexdigest(dsse_envelope.payload) 260 | } 261 | } 262 | } 263 | } 264 | end 265 | 266 | def expected_dsse_0_0_1_tlog_entry 267 | { 268 | "apiVersion" => "0.0.1", 269 | "kind" => "dsse", 270 | "spec" => { 271 | "payloadHash" => { 272 | "algorithm" => "sha256", 273 | "value" => OpenSSL::Digest::SHA256.hexdigest(dsse_envelope.payload) 274 | }, 275 | "signatures" => 276 | dsse_envelope.signatures.map do |sig| 277 | { 278 | "signature" => Internal::Util.base64_encode(sig.sig), 279 | "verifier" => Internal::Util.base64_encode(leaf_certificate.to_pem) 280 | } 281 | end 282 | } 283 | } 284 | end 285 | end 286 | end 287 | -------------------------------------------------------------------------------- /lib/sigstore/oidc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | module OIDC 19 | KNOWN_OIDC_ISSUERS = { 20 | "https://accounts.google.com" => "email", 21 | "https://oauth2.sigstore.dev/auth" => "email", 22 | "https://oauth2.sigstage.dev/auth" => "email", 23 | "https://token.actions.githubusercontent.com" => "job_workflow_ref" 24 | }.freeze 25 | private_constant :KNOWN_OIDC_ISSUERS 26 | 27 | DEFAULT_AUDIENCE = "sigstore" 28 | private_constant :DEFAULT_AUDIENCE 29 | 30 | class IdentityToken 31 | attr_reader :raw_token, :identity 32 | 33 | def initialize(raw_token) 34 | @raw_token = raw_token 35 | 36 | @unverified_claims = self.class.decode_jwt(raw_token) 37 | @iss = @unverified_claims["iss"] 38 | @nbf = @unverified_claims["nbf"] 39 | @exp = @unverified_claims["exp"] 40 | 41 | # fail early if this token isn't within its validity period 42 | raise Error::InvalidIdentityToken, "identity token is not within its validity period" unless in_validity_period? 43 | 44 | if (identity_claim = KNOWN_OIDC_ISSUERS[issuer]) 45 | unless @unverified_claims[identity_claim] 46 | raise Error::InvalidIdentityToken, "identity token is missing required claim: #{identity_claim}" 47 | end 48 | 49 | @identity = @unverified_claims[identity_claim] 50 | # https://github.com/sigstore/fulcio/blob/8311f93c01ea5b068a86d37c4bb51573289bfd69/pkg/identity/github/principal.go#L92 51 | @identity = "https://github.com/#{@identity}" if issuer == "https://token.actions.githubusercontent.com" 52 | else 53 | @identity = @unverified_claims["sub"] 54 | end 55 | end 56 | 57 | def issuer 58 | @iss 59 | end 60 | 61 | def self.decode_jwt(raw_token) 62 | # These claims are required by OpenID Connect, so 63 | # we can strongly enforce their presence. 64 | # See: https://openid.net/specs/openid-connect-basic-1_0.html#IDToken 65 | required = %w[aud sub iat exp iss] 66 | audience = DEFAULT_AUDIENCE 67 | leeway = 5 68 | 69 | _header, payload, _signature = 70 | raw_token 71 | .split(".", 3) 72 | .tap do |parts| 73 | raise Error::InvalidIdentityToken, "identity token is not a JWT" unless parts.length == 3 74 | end.map! do |part| # rubocop:disable Style/MultilineBlockChain 75 | part.unpack1("m*") 76 | rescue ArgumentError 77 | raise Error::InvalidIdentityToken, "Invalid base64 in identity token" 78 | end 79 | 80 | begin 81 | payload = JSON.parse(payload) 82 | rescue JSON::ParserError 83 | raise Error::InvalidIdentityToken, "Invalid JSON in identity token" 84 | end 85 | unless payload.is_a?(Hash) 86 | raise Error::InvalidIdentityToken, 87 | "Invalid JSON in identity token: must be a json object" 88 | end 89 | time = Time.now.to_i 90 | validate_required_claims(payload, required) 91 | validate_iat(payload["iat"], time, leeway) 92 | validate_nbf(payload["nbf"], time, leeway) 93 | validate_exp(payload["exp"], time, leeway) 94 | validate_aud(payload["aud"], audience) 95 | 96 | payload 97 | end 98 | 99 | private 100 | 101 | # Returns whether or not this `Identity` is currently within its self-stated validity period. 102 | def in_validity_period? 103 | now = Time.now.utc.to_i 104 | return false if @nbf && @nbf > now 105 | 106 | now < @exp 107 | end 108 | 109 | class << self 110 | private 111 | 112 | def validate_required_claims(payload, required) 113 | required.each do |claim| 114 | next if payload[claim] 115 | 116 | raise Error::InvalidIdentityToken, "Missing required claim in identity token: #{claim}" 117 | end 118 | end 119 | 120 | def validate_iat(iat, now, leeway) 121 | raise Error::InvalidIdentityToken, "iat claim must be an integer" unless iat.is_a?(Integer) 122 | raise Error::InvalidIdentityToken, "iat claim is in the future" if iat > now + leeway 123 | end 124 | 125 | def validate_nbf(nbf, now, leeway) 126 | raise Error::InvalidIdentityToken, "nbf claim must be an integer" unless nbf.is_a?(Integer) 127 | raise Error::InvalidIdentityToken, "nbf claim is in the future" if nbf > now + leeway 128 | end 129 | 130 | def validate_exp(exp, now, leeway) 131 | raise Error::InvalidIdentityToken, "exp claim must be an integer" unless exp.is_a?(Integer) 132 | raise Error::InvalidIdentityToken, "exp claim is in the past" if exp <= now - leeway 133 | end 134 | 135 | def validate_aud(aud, audience) 136 | aud = Array(aud) 137 | 138 | raise Error::InvalidIdentityToken, "aud claim must not be empty" if aud.empty? 139 | raise Error::InvalidIdentityToken, "aud claim must be strings" unless aud.all?(String) 140 | 141 | return if aud.include?(audience) 142 | 143 | raise Error::InvalidIdentityToken, 144 | "aud claim does not contain the expected audience #{audience.inspect}" 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/sigstore/policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | module Policy 19 | class SingleX509ExtPolicy 20 | def initialize(value) 21 | @value = value 22 | end 23 | 24 | def verify(cert) 25 | ext = cert.openssl.find_extension(oid) 26 | unless ext 27 | return VerificationFailure.new("Certificate does not contain #{self.class.name&.[](/::([^:]+)$/, 1)} " \ 28 | "(#{oid}) extension") 29 | end 30 | 31 | value = ext_value(ext) 32 | verified = value == @value 33 | unless verified 34 | return VerificationFailure.new("Certificate's #{self.class.name&.[](/::([^:]+)$/, 1)} does not match " \ 35 | "(got #{value}, expected #{@value})") 36 | end 37 | 38 | VerificationSuccess.new 39 | end 40 | 41 | if RUBY_ENGINE == "jruby" 42 | def ext_value(ext) 43 | der = ext.to_der 44 | seq = OpenSSL::ASN1.decode(der) 45 | seq.value.last.value 46 | end 47 | else 48 | def ext_value(ext) 49 | ext.value 50 | end 51 | end 52 | 53 | def oid 54 | self.class::OID # : String 55 | end 56 | end 57 | 58 | class OIDCIssuer < SingleX509ExtPolicy 59 | OID = "1.3.6.1.4.1.57264.1.1" 60 | end 61 | 62 | class SingleX509ExtDerEncodedPolicy < SingleX509ExtPolicy 63 | def ext_value(ext) 64 | OpenSSL::ASN1.decode(ext.value_der).value 65 | end 66 | end 67 | 68 | class OIDCIssuerV2 < SingleX509ExtDerEncodedPolicy 69 | OID = "1.3.6.1.4.1.57264.1.8" 70 | end 71 | 72 | class AnyOf 73 | def initialize(*policies) 74 | @policies = policies 75 | end 76 | 77 | def verify(cert) 78 | failures = [] 79 | @policies.each do |policy| 80 | result = policy.verify(cert) 81 | return result if result.verified? 82 | 83 | failures << result.reason 84 | end 85 | 86 | VerificationFailure.new("No policy matched: #{failures.join(", ")}") 87 | end 88 | end 89 | 90 | class Identity 91 | def initialize(identity:, issuer:) 92 | @identity = identity 93 | @issuer = AnyOf.new(OIDCIssuer.new(issuer), OIDCIssuerV2.new(issuer)) 94 | end 95 | 96 | def verify(cert) 97 | issuer_verified = @issuer.verify(cert) 98 | return issuer_verified unless issuer_verified.verified? 99 | 100 | san_ext = cert.extension(Sigstore::Internal::X509::Extension::SubjectAlternativeName) 101 | raise Error::InvalidCertificate, "Certificate does not contain subjectAltName extension" unless san_ext 102 | 103 | verified = san_ext.general_names.any? { |_, id| id == @identity } 104 | unless verified 105 | return VerificationFailure.new( 106 | "Certificate's SANs do not match #{@identity}; actual SANs: #{san_ext.general_names}" 107 | ) 108 | end 109 | 110 | VerificationSuccess.new 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/sigstore/rekor/checkpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | module Rekor 19 | module Checkpoint 20 | Signature = Struct.new(:name, :sig_hash, :signature, keyword_init: true) 21 | 22 | SignedCheckpoint = Struct.new(:signed_note, :checkpoint, keyword_init: true) do 23 | # @implements SignedCheckpoint 24 | 25 | def self.from_text(text) 26 | signed_note = SignedNote.from_text(text) 27 | checkpoint = LogCheckpoint.from_text(signed_note.note) 28 | 29 | new(signed_note:, checkpoint:) 30 | end 31 | end 32 | 33 | SignedNote = Struct.new(:note, :signatures, keyword_init: true) do 34 | # @implements SignedNote 35 | 36 | def self.from_text(text) 37 | separator = "\n\n" 38 | 39 | raise Error::InvalidCheckpoint, "Note must include double newline separator" unless text.include?(separator) 40 | 41 | note, signatures = text.split(separator, 2) 42 | raise Error::InvalidCheckpoint, "must contain at least one signature" if signatures.empty? 43 | raise Error::InvalidCheckpoint, "signatures must end with a newline" unless signatures.end_with?("\n") 44 | 45 | note << "\n" 46 | 47 | sig_parser = %r{^\u2014 (?[^[[:space:]]+]+) (?[0-9A-Za-z+/=-]+)\n} 48 | 49 | signatures = signatures.lines.map! do |line| 50 | raise Error::InvalidCertificate, "Invalid signature line: #{line.inspect}" unless sig_parser =~ line 51 | 52 | name = Regexp.last_match[:name] 53 | signature = Regexp.last_match[:signature] 54 | 55 | signature_bytes = signature.unpack1("m0") 56 | raise Error::InvalidCheckpoint, "too few bytes in signature" if signature_bytes.bytesize < 5 57 | 58 | sig_hash = signature_bytes.slice!(0, 4).unpack1("a4") 59 | 60 | Signature.new(name:, sig_hash:, signature: signature_bytes) 61 | end 62 | 63 | new(note:, signatures:) 64 | end 65 | 66 | def verify(rekor_keyring, key_id) 67 | data = note.encode("utf-8") 68 | signatures.each do |signature| 69 | sig_hash = key_id[0, 4] 70 | if signature.sig_hash != sig_hash 71 | raise Error::InvalidCheckpoint, 72 | "sig_hash hint #{signature.sig_hash.inspect} does not match key_id #{sig_hash.inspect}" 73 | end 74 | 75 | rekor_keyring.verify(key_id: key_id.unpack1("H*"), signature: signature.signature, data:) 76 | end 77 | end 78 | end 79 | 80 | LogCheckpoint = Struct.new(:origin, :log_size, :log_hash, :other_content, keyword_init: true) do 81 | # @implements LogCheckpoint 82 | 83 | def self.from_text(text) 84 | lines = text.strip.split("\n") 85 | 86 | raise Error::InvalidCheckpoint, "too few items in header" if lines.size < 3 87 | 88 | origin = lines.shift 89 | log_size = lines.shift.to_i 90 | root_hash = lines.shift.unpack1("m0") 91 | 92 | raise Error::InvalidCheckpoint, "empty origin" if origin.empty? 93 | 94 | new(origin:, log_size:, log_hash: root_hash, other_content: lines) 95 | end 96 | end 97 | 98 | def self.verify_checkpoint(rekor_keyring, entry) 99 | raise Error::InvalidRekorEntry, "Rekor entry has no inclusion proof" unless entry.inclusion_proof 100 | 101 | signed_checkpoint = SignedCheckpoint.from_text(entry.inclusion_proof.checkpoint.envelope) 102 | signed_checkpoint.signed_note.verify(rekor_keyring, entry.log_id.key_id) 103 | 104 | checkpoint_hash = signed_checkpoint.checkpoint.log_hash 105 | root_hash = entry.inclusion_proof.root_hash 106 | 107 | return if checkpoint_hash == root_hash 108 | 109 | raise Error::InvalidRekorEntry, "Inclusion proof contains invalid root hash: " \ 110 | "expected #{checkpoint_hash.inspect}, calculated #{root_hash.inspect}" 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/sigstore/rekor/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require "net/http" 18 | 19 | module Sigstore 20 | module Rekor 21 | class Client 22 | DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" 23 | STAGING_REKOR_URL = "https://rekor.sigstage.dev" 24 | 25 | def initialize(url:) 26 | @url = URI.join(url, "api/v1/") 27 | 28 | net = defined?(Gem::Net) ? Gem::Net : Net 29 | @session = net::HTTP.new(@url.host, @url.port) 30 | @session.use_ssl = true 31 | end 32 | 33 | def self.production 34 | new(url: DEFAULT_REKOR_URL) 35 | end 36 | 37 | def self.staging 38 | new(url: STAGING_REKOR_URL) 39 | end 40 | 41 | def log 42 | Log.new(URI.join(@url, "log/"), session: @session) 43 | end 44 | end 45 | 46 | class Log 47 | def initialize(url, session:) 48 | @url = url 49 | @session = session 50 | end 51 | 52 | def entries 53 | Entries.new(URI.join(@url, "entries/"), session: @session) 54 | end 55 | end 56 | 57 | class Entries 58 | def initialize(url, session:) 59 | @url = url 60 | @session = session 61 | end 62 | 63 | def retrieve 64 | Retrieve.new(URI.join(@url, "retrieve/"), session: @session) 65 | end 66 | 67 | def post(entry) 68 | resp = @session.post2(@url.path.chomp("/"), entry.to_json, 69 | { "Content-Type" => "application/json", "Accept" => "application/json" }) 70 | 71 | unless resp.code == "201" 72 | raise Error::FailedRekorPost, 73 | "#{resp.code} #{resp.message.inspect}\n#{JSON.pretty_generate(entry)}\n#{resp.body}" 74 | end 75 | unless resp.content_type == "application/json" 76 | raise Error::FailedRekorPost, "Unexpected content type: #{resp.content_type.inspect}" 77 | end 78 | 79 | body = JSON.parse(resp.body) 80 | Entries.decode_transparency_log_entry(body) 81 | end 82 | 83 | class Retrieve 84 | def initialize(url, session:) 85 | @url = url 86 | @session = session 87 | end 88 | 89 | def post(expected_entry) 90 | data = { entries: [expected_entry] } 91 | resp = @session.post2(@url.path, data.to_json, 92 | { "Content-Type" => "application/json", "Accept" => "application/json" }) 93 | 94 | if resp.code != "200" 95 | raise Error::FailedRekorLookup, 96 | "#{resp.code} #{resp.message.inspect}\n#{JSON.pretty_generate(data)}\n#{resp.body}" 97 | end 98 | 99 | results = JSON.parse(resp.body) 100 | 101 | results.map do |result| 102 | Entries.decode_transparency_log_entry(result) 103 | end.min_by(&:integrated_time) 104 | end 105 | end 106 | 107 | def self.decode_transparency_log_entry(response) 108 | raise ArgumentError, "response must be a Hash" unless response.is_a?(Hash) 109 | raise ArgumentError, "Received multiple entries in response" if response.size != 1 110 | 111 | _, result = response.first 112 | canonicalized_body = Internal::Util.base64_decode(result.fetch("body")) 113 | body = JSON.parse(canonicalized_body) 114 | entry = V1::TransparencyLogEntry.new 115 | entry.log_index = result.fetch("logIndex") 116 | entry.log_id = Common::V1::LogId.new 117 | entry.log_id.key_id = Internal::Util.hex_decode(result.fetch("logID")) 118 | entry.kind_version = V1::KindVersion.new 119 | entry.kind_version.kind = body.fetch("kind") 120 | entry.kind_version.version = body.fetch("apiVersion") 121 | entry.integrated_time = result.fetch("integratedTime") 122 | entry.canonicalized_body = canonicalized_body 123 | if (set = result.dig("verification", "signedEntryTimestamp")) 124 | entry.inclusion_promise = V1::InclusionPromise.new 125 | entry.inclusion_promise.signed_entry_timestamp = Internal::Util.base64_decode(set) 126 | end 127 | if (inclusion_proof = result.dig("verification", "inclusionProof")) 128 | entry.inclusion_proof = V1::InclusionProof.new 129 | entry.inclusion_proof.checkpoint = V1::Checkpoint.new 130 | entry.inclusion_proof.checkpoint.envelope = inclusion_proof.fetch("checkpoint") 131 | entry.inclusion_proof.hashes = inclusion_proof.fetch("hashes").map { |h| Internal::Util.hex_decode(h) } 132 | entry.inclusion_proof.log_index = inclusion_proof.fetch("logIndex") 133 | entry.inclusion_proof.root_hash = Internal::Util.hex_decode(inclusion_proof.fetch("rootHash")) 134 | entry.inclusion_proof.tree_size = inclusion_proof.fetch("treeSize") 135 | end 136 | 137 | entry 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/sigstore/trusted_root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require "delegate" 18 | require "openssl" 19 | 20 | require "protobug_sigstore_protos" 21 | 22 | require_relative "tuf" 23 | 24 | module Sigstore 25 | REGISTRY = Protobug::Registry.new do |registry| 26 | Sigstore::TrustRoot::V1.register_sigstore_trustroot_protos(registry) 27 | Sigstore::Bundle::V1.register_sigstore_bundle_protos(registry) 28 | end 29 | class TrustedRoot < DelegateClass(Sigstore::TrustRoot::V1::TrustedRoot) 30 | def self.production(offline: false) 31 | from_tuf(TUF::DEFAULT_TUF_URL, offline) 32 | end 33 | 34 | def self.staging(offline: false) 35 | from_tuf(TUF::STAGING_TUF_URL, offline) 36 | end 37 | 38 | def self.from_tuf(url, offline) 39 | path = TUF::TrustUpdater.new(url, offline).tap { _1.refresh unless offline }.trusted_root_path 40 | from_file(path) 41 | end 42 | 43 | def self.from_file(path) 44 | contents = Gem.read_binary(path) 45 | new Sigstore::TrustRoot::V1::TrustedRoot.decode_json(contents, registry: REGISTRY) 46 | end 47 | 48 | def rekor_keys 49 | keys = tlog_keys(tlogs).to_a 50 | raise Error::InvalidBundle, "Did not find one Rekor key" if keys.size != 1 51 | 52 | keys 53 | end 54 | 55 | def ctfe_keys 56 | keys = tlog_keys(ctlogs).to_a 57 | raise Error::InvalidBundle, "Did not find any CTFE keys" if keys.empty? 58 | 59 | keys 60 | end 61 | 62 | def fulcio_cert_chains 63 | chains = ca_keys(certificate_authorities, allow_expired: true).map do |certs| 64 | certs.map { |raw_bytes| Internal::X509::Certificate.read(raw_bytes) } 65 | end 66 | raise Error::InvalidBundle, "Fulcio certificates not found in trusted root" if chains.none?(&:any?) 67 | 68 | chains 69 | end 70 | 71 | def tlog_for_signing 72 | tlogs.find do |ctlog| 73 | timerange_valid?(ctlog.public_key.valid_for, allow_expired: false) 74 | end 75 | end 76 | 77 | def certificate_authority_for_signing 78 | certificate_authorities.find do |ca| 79 | timerange_valid?(ca.valid_for, allow_expired: false) 80 | end 81 | end 82 | 83 | private 84 | 85 | def tlog_keys(tlogs) 86 | return enum_for(__method__, tlogs) unless block_given? 87 | 88 | tlogs.each do |transparency_log_instance| 89 | key = transparency_log_instance.public_key 90 | parsed_key = Internal::Key.from_key_details(key.key_details, key.raw_bytes) 91 | yield parsed_key if parsed_key 92 | end 93 | end 94 | 95 | def ca_keys(certificate_authorities, allow_expired:) 96 | return enum_for(__method__, certificate_authorities, allow_expired:) unless block_given? 97 | 98 | certificate_authorities.each do |ca| 99 | next unless timerange_valid?(ca.valid_for, allow_expired:) 100 | 101 | yield ca.cert_chain.certificates.map(&:raw_bytes) 102 | end 103 | end 104 | 105 | def timerange_valid?(period, allow_expired:) 106 | now = Time.now.utc 107 | return true unless period 108 | return false if now < period.start.to_time 109 | return true if allow_expired 110 | return false if period.end && now > period.end.to_time 111 | 112 | true 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/sigstore/tuf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "tuf/updater" 18 | require "tempfile" 19 | require "uri" 20 | require "net/http" 21 | require "rubygems/remote_fetcher" 22 | 23 | module Sigstore 24 | module TUF 25 | DEFAULT_TUF_URL = "https://tuf-repo-cdn.sigstore.dev" 26 | STAGING_TUF_URL = "https://tuf-repo-cdn.sigstage.dev" 27 | 28 | class TrustUpdater 29 | include Loggable 30 | 31 | Net = defined?(Gem::Net) ? Gem::Net : Net 32 | 33 | attr_reader :updater 34 | 35 | def initialize(metadata_url, offline, metadata_dir: nil, targets_dir: nil, target_base_url: nil, 36 | config: UpdaterConfig.new) 37 | @repo_url = metadata_url 38 | 39 | default_metadata_dir, default_targets_dir = get_dirs(metadata_url) unless metadata_dir && targets_dir 40 | @metadata_dir = metadata_dir || default_metadata_dir 41 | @targets_dir = targets_dir || default_targets_dir 42 | 43 | @offline = offline 44 | 45 | rsrc_prefix = if @repo_url == DEFAULT_TUF_URL 46 | "prod" 47 | elsif @repo_url == STAGING_TUF_URL 48 | "staging" 49 | end 50 | 51 | FileUtils.mkdir_p @metadata_dir 52 | FileUtils.mkdir_p @targets_dir 53 | 54 | if rsrc_prefix 55 | tuf_root = File.join(@metadata_dir, "root.json") 56 | 57 | unless File.exist?(tuf_root) 58 | File.open(tuf_root, "wb") do |f| 59 | File.open(File.expand_path("../../data/_store/#{rsrc_prefix}/root.json", __dir__), "rb") do |r| 60 | logger.info { "Copying root.json from #{r.path} to #{f.path}" } 61 | IO.copy_stream(r, f) 62 | end 63 | end 64 | end 65 | 66 | trusted_root_target = File.join(@targets_dir, "trusted_root.json") 67 | 68 | unless File.exist?(trusted_root_target) 69 | File.open(trusted_root_target, "wb") do |f| 70 | File.open(File.expand_path("../../data/_store/#{rsrc_prefix}/trusted_root.json", __dir__), 71 | "rb") do |r| 72 | logger.info { "Copying trusted_root.json from #{r.path} to #{f.path}" } 73 | IO.copy_stream(r, f) 74 | end 75 | end 76 | end 77 | end 78 | 79 | return if @offline 80 | 81 | @updater = Updater.new( 82 | metadata_dir: @metadata_dir, 83 | metadata_base_url: @repo_url, 84 | target_base_url: (target_base_url && URI.parse(target_base_url)) || 85 | URI.join("#{@repo_url.to_s.chomp("/")}/", "targets/"), 86 | target_dir: @targets_dir, 87 | fetcher: method(:fetch), 88 | config: 89 | ) 90 | end 91 | 92 | def get_dirs(url) 93 | app_name = "sigstore-ruby" 94 | app_author = "sigstore" 95 | 96 | repo_base = URI.encode_uri_component(url) 97 | home = Dir.home 98 | 99 | data_home = ENV.fetch("XDG_DATA_HOME", File.join(home, ".local", "share")) 100 | cache_home = ENV.fetch("XDG_CACHE_HOME", File.join(home, ".cache")) 101 | tuf_data_dir = File.join(data_home, app_name, app_author, "tuf") 102 | tuf_cache_dir = File.join(cache_home, app_name, app_author, "tuf") 103 | 104 | [File.join(tuf_data_dir, repo_base), File.join(tuf_cache_dir, repo_base)] 105 | end 106 | 107 | def trusted_root_path 108 | unless @updater 109 | logger.info { "Offline mode: using cached trusted root" } 110 | return File.join(@targets_dir, "trusted_root.json") 111 | end 112 | 113 | root_info = @updater.get_targetinfo("trusted_root.json") 114 | raise Error::NoTrustedRoot, "Unsupported TUF configuration: no trusted_root.json" unless root_info 115 | 116 | path = @updater.find_cached_target(root_info) 117 | path ||= @updater.download_target(root_info) 118 | 119 | path 120 | end 121 | 122 | def refresh 123 | raise ArgumentError, "Offline mode: cannot refresh" if @offline || !@updater 124 | 125 | @updater.refresh 126 | end 127 | 128 | private 129 | 130 | def fetch(uri) 131 | uri = Gem::Uri.new uri 132 | raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" unless %w[http https].include?(uri.scheme) 133 | 134 | fetcher = Gem::RemoteFetcher.fetcher 135 | begin 136 | response = fetcher.request(uri, Net::HTTP::Get, nil) do 137 | nil 138 | end 139 | response.uri = uri 140 | case response 141 | when Net::HTTPOK 142 | nil 143 | when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther, 144 | Net::HTTPTemporaryRedirect 145 | raise Error::UnsuccessfulResponse.new("should redirects be supported?", response) 146 | else 147 | raise Error::UnsuccessfulResponse.new("FetchError: #{response.code}", response) 148 | end 149 | response.body 150 | rescue (defined?(Gem::Timeout::Error) ? Gem::Timeout::Error : Timeout::Error), 151 | IOError, SocketError, SystemCallError, 152 | *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e 153 | raise Error::RemoteConnection, e.message 154 | end 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | module TUF 19 | class UpdaterConfig 20 | attr_reader :max_root_rotations, :max_delegations, :root_max_length, :timestamp_max_length, :snapshot_max_length, 21 | :targets_max_length, :prefix_targets_with_hash, :envelope_type, :app_user_agent 22 | 23 | def initialize( 24 | max_root_rotations: 32, 25 | max_delegations: 32, 26 | root_max_length: 512_000, # bytes 27 | timestamp_max_length: 16_384, # bytes 28 | snapshot_max_length: 2_000_000, # bytes 29 | targets_max_length: 5_000_000, # bytes 30 | prefix_targets_with_hash: true, 31 | envelope_type: :metadata, 32 | app_user_agent: nil 33 | ) 34 | @max_root_rotations = max_root_rotations 35 | @max_delegations = max_delegations 36 | @root_max_length = root_max_length 37 | @timestamp_max_length = timestamp_max_length 38 | @snapshot_max_length = snapshot_max_length 39 | @targets_max_length = targets_max_length 40 | @prefix_targets_with_hash = prefix_targets_with_hash 41 | @envelope_type = envelope_type 42 | @app_user_agent = app_user_agent 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "../error" 18 | 19 | module Sigstore::TUF 20 | class Error < ::Sigstore::Error 21 | # An error with a repository's state, such as a missing file. 22 | class RepositoryError < Error; end 23 | 24 | class LengthOrHashMismatch < RepositoryError; end 25 | class ExpiredMetadata < RepositoryError; end 26 | class BadVersionNumber < RepositoryError; end 27 | class EqualVersionNumber < BadVersionNumber; end 28 | class TooFewSignatures < RepositoryError; end 29 | 30 | class BadUpdateOrder < Error; end 31 | class InvalidData < Error; end 32 | class DuplicateKeys < Error; end 33 | 34 | # An error occurred while attempting to download a file. 35 | class DownloadError < Error; end 36 | 37 | class Fetch < Error; end 38 | class RemoteConnection < Fetch; end 39 | 40 | class UnsuccessfulResponse < Fetch 41 | attr_reader :response 42 | 43 | def initialize(message, response) 44 | super(message) 45 | @response = response 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "error" 18 | 19 | module Sigstore::TUF 20 | module BaseFile 21 | def self.included(base) 22 | base.extend(ClassMethods) 23 | super 24 | end 25 | 26 | module ClassMethods 27 | def verify_hashes(data, expected_hashed) 28 | expected_hashed.each do |algorithm, expected_hash| 29 | actual_hash = Digest(algorithm.upcase).hexdigest(data) 30 | unless actual_hash == expected_hash 31 | raise Error::LengthOrHashMismatch, 32 | "observed hash #{actual_hash} does not match expected hash #{expected_hash}" 33 | end 34 | end 35 | end 36 | 37 | def verify_length(data, expected_length) 38 | actual_length = data.bytesize 39 | return if actual_length == expected_length 40 | 41 | raise Error::LengthOrHashMismatch, 42 | "Observed length #{actual_length} does not match expected length #{expected_length}" 43 | end 44 | 45 | def validate_hashes(hashes) 46 | raise ArgumentError, "hashes must be non-empty" if hashes.empty? 47 | 48 | hashes.each do |algorithm, hash| 49 | raise TypeError, "hashes items must be strings" unless algorithm.is_a?(String) && hash.is_a?(String) 50 | end 51 | end 52 | 53 | def validate_length(length) 54 | return unless length.negative? 55 | 56 | raise ArgumentError, "length must be a non-negative integer, got #{length.inspect}" 57 | end 58 | end 59 | end 60 | 61 | module MetaFile 62 | def self.included(base) 63 | base.include(BaseFile) 64 | base.extend(ClassMethods) 65 | super 66 | end 67 | 68 | def initialize(version: 1, length: nil, hashes: nil, unrecognized_fields: {}) 69 | @version = version 70 | @length = length 71 | @hashes = hashes 72 | @unrecognized_fields = unrecognized_fields 73 | 74 | raise ArgumentError, "Metafile version must be positive, got #{@version}" if @version <= 0 75 | 76 | self.class.validate_length(@length) unless @length.nil? 77 | self.class.validate_hashes(@hashes) unless @hashes.nil? 78 | end 79 | 80 | def verify_length_and_hashes(data) 81 | self.class.verify_length(data, @length) if @length 82 | self.class.verify_hashes(data, @hashes) if @hashes 83 | end 84 | 85 | module ClassMethods 86 | def from_hash(meta_dict) 87 | version = meta_dict.fetch("version") { raise KeyError, "version is required, given #{meta_dict.inspect}" } 88 | length = meta_dict.fetch("length", nil) 89 | hashes = meta_dict.fetch("hashes", nil) 90 | 91 | new(version:, length:, hashes:, 92 | unrecognized_fields: meta_dict.slice(*(meta_dict.keys - %w[version length hashes]))) 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore::TUF 18 | class Keys 19 | include Enumerable 20 | 21 | def initialize(keys) 22 | @keys = keys.to_h do |key_id, key_data| 23 | key_type = key_data.fetch("keytype") 24 | scheme = key_data.fetch("scheme") 25 | keyval = key_data.fetch("keyval") 26 | public_key_data = keyval.fetch("public") 27 | 28 | key = Sigstore::Internal::Key.read(key_type, scheme, public_key_data, key_id:) 29 | 30 | [key_id, key] 31 | end 32 | end 33 | 34 | def fetch(key_id) 35 | @keys.fetch(key_id) 36 | end 37 | 38 | def each(&) 39 | @keys.each(&) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/roles.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore::TUF 18 | class Roles 19 | include Enumerable 20 | 21 | def initialize(data, keys) 22 | @roles = 23 | case data 24 | when Hash # root roles 25 | data.to_h do |role_name, role_data| 26 | role_data = role_data.merge("name" => role_name, "paths" => nil) 27 | role = Role.new(role_data, keys) 28 | [role.name, role] 29 | end 30 | when Array # targets roles 31 | data.to_h do |role_data| 32 | role = Role.new(role_data, keys) 33 | [role.name, role] 34 | end 35 | else 36 | raise ArgumentError, "Unexpected data: #{data.inspect}" 37 | end 38 | end 39 | 40 | def each(&) 41 | @roles.each(&) 42 | end 43 | 44 | def verify_delegate(type, bytes, signatures) 45 | role = fetch(type) 46 | role.verify_delegate(type, bytes, signatures) 47 | end 48 | 49 | def fetch(name) 50 | @roles.fetch(name) 51 | end 52 | 53 | def for_target(target_path) 54 | select do |_, role| 55 | # TODO: this needs to be tested 56 | role.paths.any? { |path| File.fnmatch?(path, target_path, File::FNM_PATHNAME) } 57 | end.to_h 58 | end 59 | end 60 | 61 | class Role 62 | include Sigstore::Loggable 63 | 64 | attr_reader :keys, :name, :paths, :threshold 65 | 66 | def initialize(data, keys) 67 | @name = data.fetch("name") 68 | @paths = data.fetch("paths") 69 | @threshold = data.fetch("threshold") 70 | @keys = data.fetch("keyids").to_h { |key_id| [key_id, keys.fetch(key_id)] } 71 | @terminating = data.fetch("terminating", false) 72 | end 73 | 74 | def terminating? 75 | @terminating 76 | end 77 | 78 | def verify_delegate(type, bytes, signatures) 79 | if (duplicate_keys = signatures.map { |sig| sig.fetch("keyid") }.tally.select { |_, count| count > 1 }).any? 80 | raise Error::DuplicateKeys, "Duplicate keys found in signatures: #{duplicate_keys.inspect}" 81 | end 82 | 83 | count = signatures.count do |signature| 84 | key_id = signature.fetch("keyid") 85 | unless @keys.include?(key_id) 86 | logger.warn "Unknown key_id=#{key_id.inspect} in signatures for #{type}" 87 | next 88 | end 89 | 90 | key = @keys.fetch(key_id) 91 | signature_bytes = [signature.fetch("sig")].pack("H*") 92 | verified = key.verify("sha256", signature_bytes, bytes) 93 | 94 | logger.debug do 95 | "key_id=#{key_id.inspect} type=#{type} verified=#{verified}" 96 | end 97 | verified 98 | end 99 | 100 | return unless count < @threshold 101 | 102 | raise Error::TooFewSignatures, 103 | "Not enough signatures: found #{count} out of threshold=#{@threshold} for #{type}" 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require "time" 18 | 19 | require_relative "keys" 20 | require_relative "roles" 21 | require_relative "../internal/key" 22 | 23 | module Sigstore::TUF 24 | class Root 25 | include Sigstore::Loggable 26 | 27 | TYPE = "root" 28 | attr_reader :version, :consistent_snapshot, :expires 29 | 30 | def initialize(data) 31 | type = data.fetch("_type") 32 | raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE 33 | 34 | @spec_version = data.fetch("spec_version") { raise Error::InvalidData, "root missing spec_version" } 35 | @consistent_snapshot = data.fetch("consistent_snapshot") do 36 | raise Error::InvalidData, "root missing consistent_snapshot" 37 | end 38 | @version = data.fetch("version") { raise Error::InvalidData, "root missing version" } 39 | @expires = Time.iso8601(data.fetch("expires") { raise Error::InvalidData, "root missing expires" }) 40 | keys = Keys.new data.fetch("keys") 41 | @roles = Roles.new data.fetch("roles"), keys 42 | @unrecognized_fields = data.fetch("unrecognized_fields", {}) 43 | end 44 | 45 | def verify_delegate(type, bytes, signatures) 46 | @roles.verify_delegate(type, bytes, signatures) 47 | end 48 | 49 | def expired?(reference_time) 50 | @expires < reference_time 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/snapshot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "file" 18 | 19 | module Sigstore::TUF 20 | # The class for the Snapshot role 21 | class Snapshot 22 | TYPE = "snapshot" 23 | 24 | attr_reader :version, :meta 25 | 26 | def initialize(data) 27 | type = data.fetch("_type") 28 | raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE 29 | 30 | @version = data.fetch("version") 31 | @expires = Time.iso8601 data.fetch("expires") 32 | @meta = data.fetch("meta").transform_values { Meta.from_hash(_1) } 33 | end 34 | 35 | def expired?(reference_time) 36 | @expires < reference_time 37 | end 38 | 39 | class Meta 40 | include MetaFile 41 | 42 | attr_reader :version 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/targets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "file" 18 | require_relative "keys" 19 | require_relative "roles" 20 | 21 | module Sigstore::TUF 22 | class Targets 23 | TYPE = "targets" 24 | 25 | attr_reader :version, :targets, :delegations 26 | 27 | def initialize(data) 28 | type = data.fetch("_type") 29 | raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE 30 | 31 | @version = data.fetch("version") 32 | @expires = Time.iso8601 data.fetch("expires") 33 | @targets = data.fetch("targets").to_h { |k, v| [k, Target.new(v, k)] } 34 | @delegations = Delegations.new(data.fetch("delegations", {})) 35 | 36 | @unrecognized_fields = data.fetch("unrecognized_fields", {}) 37 | end 38 | 39 | def expired?(reference_time) 40 | @expires < reference_time 41 | end 42 | 43 | def verify_delegate(type, bytes, signatures) 44 | role = @delegations.fetch(type) 45 | role.verify_delegate(type, bytes, signatures) 46 | end 47 | 48 | class Target 49 | attr_reader :path, :hashes 50 | 51 | include BaseFile 52 | 53 | def initialize(data, path) 54 | @path = path 55 | @length = data.fetch("length") 56 | @hashes = data.fetch("hashes") 57 | end 58 | 59 | def verify_length_and_hashes(data) 60 | self.class.verify_length(data, @length) 61 | self.class.verify_hashes(data, @hashes) 62 | end 63 | end 64 | 65 | class Delegations 66 | def initialize(data) 67 | keys = Keys.new data.fetch("keys", {}) 68 | @roles = Roles.new data.fetch("roles", []), keys 69 | end 70 | 71 | def roles_for_target(target_path) 72 | @roles.for_target(target_path) 73 | end 74 | 75 | def any? 76 | @roles.any? 77 | end 78 | 79 | def fetch(name) 80 | @roles.fetch(name) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/timestamp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore::TUF 18 | class Timestamp 19 | TYPE = "timestamp" 20 | 21 | attr_reader :version, :spec_version, :expires, :snapshot_meta, :unrecognized_fields 22 | 23 | def initialize(data) 24 | type = data.fetch("_type") 25 | raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE 26 | 27 | @version = data.fetch("version") 28 | @spec_version = data.fetch("spec_version") 29 | @expires = Time.iso8601 data.fetch("expires") 30 | meta_dict = data.fetch("meta") 31 | @snapshot_meta = Snapshot::Meta.from_hash(meta_dict["snapshot.json"]) 32 | @unrecognized_fields = data.fetch("unrecognized_fields", {}) 33 | end 34 | 35 | def expired?(reference_time) 36 | @expires < reference_time 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/trusted_metadata_set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "error" 18 | require_relative "root" 19 | require_relative "../internal/json" 20 | 21 | require "json" 22 | 23 | module Sigstore::TUF 24 | class TrustedMetadataSet 25 | include Sigstore::Loggable 26 | 27 | def initialize(root_data, envelope_type, reference_time: Time.now.utc) 28 | @trusted_set = {} 29 | @reference_time = reference_time 30 | @envelope_type = envelope_type 31 | 32 | logger.debug { "Loading trusted root" } 33 | load_trusted_root(root_data) 34 | end 35 | 36 | def root 37 | @trusted_set.fetch("root") { raise Error::InvalidData, "missing root metadata" } 38 | end 39 | 40 | def root=(data) 41 | raise Error::BadUpdateOrder, "cannot update root after timestamp" if @trusted_set.key?("timestamp") 42 | 43 | metadata, canonical_signed, signatures = load_data(Root, data, root) 44 | metadata.verify_delegate("root", canonical_signed, signatures) 45 | raise Error::BadVersionNumber, "root version did not increment by one" if metadata.version != root.version + 1 46 | 47 | @trusted_set["root"] = metadata 48 | 49 | logger.debug { "Updated root v#{metadata.version}" } 50 | end 51 | 52 | def snapshot 53 | @trusted_set.fetch("snapshot") 54 | end 55 | 56 | def timestamp 57 | @trusted_set.fetch("timestamp") 58 | end 59 | 60 | def timestamp=(data) 61 | raise Error::BadUpdateOrder, "cannot update timestamp after snapshot" if @trusted_set.key?("snapshot") 62 | 63 | if root.expired?(@reference_time) 64 | raise Error::ExpiredMetadata, 65 | "final root.json expired at #{root.expires}, is #{@reference_time}" 66 | end 67 | 68 | metadata, = load_data(Timestamp, data, root) 69 | 70 | if include?(Timestamp::TYPE) 71 | if metadata.version < timestamp.version 72 | raise Error::BadVersionNumber, 73 | "timestamp version less than metadata version" 74 | end 75 | raise Error::EqualVersionNumber if metadata.version == timestamp.version 76 | 77 | snapshot_meta = timestamp.snapshot_meta 78 | new_snapshot_meta = metadata.snapshot_meta 79 | if new_snapshot_meta.version < snapshot_meta.version 80 | raise Error::BadVersionNumber, "snapshot version did not increase" 81 | end 82 | end 83 | 84 | @trusted_set["timestamp"] = metadata 85 | check_final_timestamp 86 | end 87 | 88 | def snapshot=(data, trusted: false) 89 | raise Error::BadUpdateOrder, "cannot update snapshot before timestamp" unless @trusted_set.key?("timestamp") 90 | raise Error::BadUpdateOrder, "cannot update snapshot after targets" if @trusted_set.key?("targets") 91 | 92 | check_final_timestamp 93 | 94 | snapshot_meta = timestamp.snapshot_meta 95 | 96 | snapshot_meta.verify_length_and_hashes(data) unless trusted 97 | 98 | new_snapshot, = load_data(Snapshot, data, root) 99 | 100 | # If an existing trusted snapshot is updated, check for rollback attack 101 | if include?(Snapshot::TYPE) 102 | snapshot.meta.each do |filename, file_info| 103 | new_file_info = new_snapshot.meta[filename] 104 | raise Error::RepositoryError, "new snapshot is missing info for #{filename}" unless new_file_info 105 | 106 | if new_file_info.version < file_info.version 107 | raise Error::BadVersionNumber, "expected #{filename} v#{new_file_info.version}, got v#{file_info.version}" 108 | end 109 | end 110 | end 111 | 112 | @trusted_set["snapshot"] = new_snapshot 113 | logger.debug { "Updated snapshot v#{new_snapshot.version}" } 114 | check_final_snapshot 115 | end 116 | 117 | def include?(type) 118 | @trusted_set.key?(type) 119 | end 120 | 121 | def [](role) 122 | @trusted_set.fetch(role) 123 | end 124 | 125 | def update_delegated_targets(data, role, parent_role) 126 | raise Error::BadUpdateOrder, "cannot update targets before snapshot" unless @trusted_set.key?("snapshot") 127 | 128 | check_final_snapshot 129 | 130 | delegator = @trusted_set[parent_role] 131 | logger.debug { "Updating #{role} delegated by #{parent_role.inspect} to #{delegator.class}" } 132 | raise Error::BadUpdateOrder, "cannot load targets before delegator" unless delegator 133 | 134 | meta = snapshot.meta["#{role}.json"] 135 | raise Error::RepositoryError, "no metadata for role #{role} in snapshot" unless meta 136 | 137 | meta.verify_length_and_hashes(data) 138 | 139 | new_delegate, = load_data(Targets, data, delegator, role) 140 | version = new_delegate.version 141 | raise Error::BadVersionNumber, "expected #{role} v#{meta.version}, got v#{version}" if version != meta.version 142 | 143 | raise Error::ExpiredMetadata, "new #{role} is expired" if new_delegate.expired?(@reference_time) 144 | 145 | @trusted_set[role] = new_delegate 146 | logger.debug { "Updated #{role} v#{version}" } 147 | new_delegate 148 | end 149 | 150 | private 151 | 152 | def load_trusted_root(data) 153 | root, canonical_signed, signatures = load_data(Root, data, nil) 154 | # verify the new root is signed by itself 155 | root.verify_delegate("root", canonical_signed, signatures) 156 | 157 | @trusted_set["root"] = root 158 | end 159 | 160 | def load_data(type, data, delegator, role_name = nil) 161 | metadata = JSON.parse(data) 162 | signed = metadata.fetch("signed") 163 | unless signed["_type"] == type::TYPE 164 | raise Error::InvalidData, 165 | "Expected type to be #{type::TYPE}, got #{signed["_type"].inspect}" 166 | end 167 | 168 | signatures = metadata.fetch("signatures") 169 | metadata = type.new(signed) 170 | canonical_signed = Sigstore::Internal::JSON.canonical_generate(signed) 171 | delegator&.verify_delegate(role_name || type::TYPE, canonical_signed, signatures) 172 | [metadata, canonical_signed, signatures] 173 | rescue JSON::ParserError => e 174 | raise Error::InvalidData, "Failed to parse #{type}: #{e.message}" 175 | end 176 | 177 | def check_final_timestamp 178 | return unless timestamp.expired?(@reference_time) 179 | 180 | raise Error::ExpiredMetadata, 181 | "final timestamp.json is expired (expired at #{timestamp.expires} vs reference time #{@reference_time})" 182 | end 183 | 184 | def check_final_snapshot 185 | raise Error::ExpiredMetadata, "final snapshot.json is expired" if snapshot.expired?(@reference_time) 186 | 187 | snapshot_meta = timestamp.snapshot_meta 188 | return if snapshot.version == snapshot_meta.version 189 | 190 | raise Error::BadVersionNumber, "expected snapshot version #{snapshot_meta.version}, got #{snapshot.version}" 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/sigstore/tuf/updater.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require_relative "config" 18 | require_relative "trusted_metadata_set" 19 | require_relative "root" 20 | require_relative "snapshot" 21 | require_relative "targets" 22 | require_relative "timestamp" 23 | 24 | module Sigstore::TUF 25 | class Updater 26 | include Sigstore::Loggable 27 | 28 | def initialize(metadata_dir:, metadata_base_url:, target_base_url:, target_dir:, fetcher:, 29 | config: UpdaterConfig.new) 30 | @dir = metadata_dir 31 | @metadata_base_url = "#{metadata_base_url.to_s.chomp("/")}/" 32 | @target_dir = target_dir 33 | @target_base_url = target_base_url && "#{target_base_url.to_s.chomp("/")}/" 34 | 35 | @fetcher = fetcher 36 | @config = config 37 | 38 | unless %i[metadata simple].include? @config.envelope_type 39 | raise ArgumentError, "Unsupported envelope type: #{@config[:envelope_type].inspect}" 40 | end 41 | 42 | data = load_local_metadata("root") 43 | @trusted_set = TrustedMetadataSet.new(data, "metadata", reference_time: Time.now) 44 | end 45 | 46 | def refresh 47 | load_root 48 | load_timestamp 49 | load_snapshot 50 | load_targets(Targets::TYPE, Root::TYPE) 51 | end 52 | 53 | def get_targetinfo(target_path) 54 | refresh unless @trusted_set.include? Targets::TYPE 55 | preorder_depth_first_walk(target_path) 56 | end 57 | 58 | def find_cached_target(target_info, filepath = nil) 59 | filepath ||= generate_target_file_path(target_info) 60 | 61 | begin 62 | data = File.binread(filepath) 63 | target_info.verify_length_and_hashes(data) 64 | filepath 65 | rescue Errno::ENOENT, Error::LengthOrHashMismatch => e 66 | logger.debug { "No cached target at #{filepath}: #{e.class} #{e.message}" } 67 | nil 68 | end 69 | end 70 | 71 | def download_target(target_info, filepath = nil, target_base_url = nil) 72 | target_base_url ||= @target_base_url 73 | raise ArgumentError, "No target_base_url set" unless target_base_url 74 | 75 | if (cached_target = find_cached_target(target_info, filepath)) 76 | return cached_target 77 | end 78 | 79 | filepath ||= generate_target_file_path(target_info) 80 | 81 | target_filepath = target_info.path 82 | consistent_snapshot = @trusted_set.root.consistent_snapshot 83 | 84 | if consistent_snapshot && @config.prefix_targets_with_hash 85 | hashes = target_info.hashes.values 86 | dir, sep, basename = target_filepath.rpartition("/") 87 | target_filepath = "#{dir}#{sep}#{hashes.first}.#{basename}" 88 | end 89 | 90 | full_url = URI.join(target_base_url, target_filepath) 91 | begin 92 | resp_body = @fetcher.call(full_url) 93 | target_info.verify_length_and_hashes(resp_body) 94 | 95 | # TODO: atomic write 96 | File.binwrite(filepath, resp_body) 97 | rescue Error::Fetch => e 98 | raise Error::Fetch, 99 | "Failed to download target #{target_info.inspect} #{target_filepath.inspect} from #{e.response.uri}: " \ 100 | "#{e.message}" 101 | end 102 | logger.info { "Downloaded #{target_filepath} to #{filepath}" } 103 | filepath 104 | end 105 | 106 | private 107 | 108 | def load_local_metadata(role_name) 109 | encoded_name = URI.encode_www_form_component(role_name) 110 | 111 | File.binread(File.join(@dir, "#{encoded_name}.json")) 112 | end 113 | 114 | def load_root 115 | lower_bound = @trusted_set.root.version + 1 116 | upper_bound = lower_bound - 1 + @config.max_root_rotations 117 | 118 | lower_bound.upto(upper_bound) do |version| 119 | data = download_metadata(Root::TYPE, version) 120 | rescue Error::UnsuccessfulResponse => e 121 | logger.debug { "Failed to download root metadata v#{version}: #{e.class} #{e.message}" } 122 | break if %w[403 404].include?(e.response.code) 123 | 124 | raise 125 | else 126 | @trusted_set.root = data 127 | persist_metadata(Root::TYPE, data) 128 | end 129 | end 130 | 131 | def load_timestamp 132 | begin 133 | data = load_local_metadata(Timestamp::TYPE) 134 | @trusted_set.timestamp = data 135 | rescue Errno::ENOENT 136 | logger.debug "No local timestamp found" 137 | rescue Error::RepositoryError => e 138 | logger.debug "Local timestamp not valid as final: #{e.class} #{e.message}" 139 | end 140 | 141 | data = download_metadata(Timestamp::TYPE, nil) 142 | 143 | begin 144 | @trusted_set.timestamp = data 145 | rescue Error::EqualVersionNumber 146 | logger.debug "Timestamp version did not increase" 147 | return 148 | end 149 | 150 | persist_metadata(Timestamp::TYPE, data) 151 | end 152 | 153 | def load_snapshot 154 | data = load_local_metadata(Snapshot::TYPE) 155 | @trusted_set.send(:snapshot=, data, trusted: true) 156 | logger.debug "Loaded snapshot from local metadata" 157 | rescue Errno::ENOENT, Error::RepositoryError => e 158 | logger.debug "Local snapshot not valid as final: #{e.class} #{e.message}" 159 | 160 | snapshot_meta = @trusted_set.timestamp.snapshot_meta 161 | version = snapshot_meta.version if @trusted_set.root.consistent_snapshot 162 | 163 | data = download_metadata(Snapshot::TYPE, version) 164 | @trusted_set.snapshot = data 165 | persist_metadata(Snapshot::TYPE, data) 166 | end 167 | 168 | def load_targets(role, parent_role) 169 | if @trusted_set.include?(role) 170 | logger.debug { "Returning cached targets for #{role}" } 171 | return @trusted_set[role] 172 | end 173 | 174 | begin 175 | data = load_local_metadata(role) 176 | @trusted_set.update_delegated_targets(data, role, parent_role).tap do 177 | logger.debug { "Loaded targets for #{role} from local metadata" } 178 | end 179 | rescue Errno::ENOENT, Error::RepositoryError => e 180 | logger.debug { "No local targets for #{role}, fetching: #{e.class} #{e.message}" } 181 | 182 | snapshot = @trusted_set.snapshot 183 | metainfo = snapshot.meta["#{role}.json"] 184 | raise Error::RepositoryError, "role #{role} was delegated but is not part of snapshot" unless metainfo 185 | 186 | version = metainfo.version if @trusted_set.root.consistent_snapshot 187 | data = download_metadata(role, version) 188 | delegated_targets = @trusted_set.update_delegated_targets(data, role, parent_role) 189 | persist_metadata(role, data) 190 | delegated_targets 191 | end 192 | end 193 | 194 | def download_metadata(role_name, version) 195 | url = metadata_url(role_name, version) 196 | 197 | logger.debug { "Downloading metadata for #{role_name} from #{url}" } 198 | 199 | @fetcher.call(url) 200 | end 201 | 202 | def metadata_url(role_name, version) 203 | encoded_name = URI.encode_www_form_component(role_name) 204 | if version.nil? 205 | URI.join(@metadata_base_url, "#{encoded_name}.json") 206 | else 207 | URI.join(@metadata_base_url, "#{version}.#{encoded_name}.json") 208 | end 209 | end 210 | 211 | def persist_metadata(role_name, data) 212 | logger.debug { "Persisting metadata for #{role_name}" } 213 | 214 | encoded_name = URI.encode_www_form_component(role_name) 215 | filename = File.join(@dir, "#{encoded_name}.json") 216 | Tempfile.create("", @dir) do |f| 217 | f.binmode 218 | f.write(data) 219 | f.close 220 | 221 | File.rename(f.path, filename) 222 | end 223 | end 224 | 225 | def preorder_depth_first_walk(target_path) 226 | logger.debug { "Searching for target #{target_path}" } 227 | 228 | delegations_to_visit = [[Targets::TYPE, Root::TYPE]] 229 | visited_role_names = Set.new 230 | 231 | while delegations_to_visit.any? && visited_role_names.size < @config.max_delegations 232 | role_name, parent_role = delegations_to_visit.pop 233 | next if visited_role_names.include?(role_name) 234 | 235 | targets = load_targets(role_name, parent_role) 236 | target = targets.targets.fetch(target_path, nil) 237 | 238 | return target if target 239 | 240 | visited_role_names.add(role_name) 241 | 242 | next unless targets.delegations.any? 243 | 244 | child_roles_to_visit = [] 245 | 246 | targets.delegations.roles_for_target(target_path).each do |child_name, delegated_role| 247 | child_roles_to_visit << [child_name, role_name] 248 | next unless delegated_role.terminating? 249 | 250 | logger.debug { "Terminating delegation found for #{child_name}" } 251 | delegations_to_visit.clear 252 | break 253 | end 254 | 255 | delegations_to_visit.concat child_roles_to_visit.reverse 256 | end 257 | 258 | logger.warn { "Max delegations reached, stopping search" } if delegations_to_visit.any? 259 | 260 | nil 261 | end 262 | 263 | def generate_target_file_path(target_info) 264 | raise ArgumentError, "target_dir not set" unless @target_dir 265 | raise ArgumentError, "target_info required" unless target_info 266 | 267 | filename = URI.encode_www_form_component(target_info.path) 268 | File.join(@target_dir, filename) 269 | end 270 | end 271 | end 272 | -------------------------------------------------------------------------------- /lib/sigstore/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2024 The Sigstore Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | module Sigstore 18 | VERSION = "0.2.2" 19 | end 20 | -------------------------------------------------------------------------------- /sigstore.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/sigstore/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "sigstore" 7 | spec.version = Sigstore::VERSION 8 | spec.authors = ["The Sigstore Authors", "Samuel Giddins"] 9 | spec.email = [nil, "segiddins@segiddins.me"] 10 | 11 | spec.summary = "A pure-ruby implementation of sigstore signature verification" 12 | spec.homepage = "https://github.com/sigstore/sigstore-ruby" 13 | spec.license = "Apache-2.0" 14 | spec.required_ruby_version = ">= 3.2.0" 15 | 16 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 17 | 18 | spec.metadata["homepage_uri"] = spec.homepage 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = IO.popen(["git", "-C", __dir__, "ls-files", "-z"], &:read).split("\x0").reject do |f| 23 | (File.expand_path(f) == __FILE__) || 24 | f.start_with?(*%w[bin/ test/ spec/ features/ fixtures/ . Rakefile Gemfile cli/]) 25 | end 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "logger" 31 | spec.add_dependency "net-http" 32 | spec.add_dependency "protobug_sigstore_protos", "~> 0.1.0" 33 | spec.add_dependency "uri" 34 | 35 | spec.metadata["rubygems_mfa_required"] = "true" 36 | end 37 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /tuf-conformance/ 2 | /sigstore-conformance/ 3 | /conformance_invocations 4 | -------------------------------------------------------------------------------- /test/sigstore/conformance_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/cli" 5 | 6 | class Sigstore::ConformanceTest < Test::Unit::TestCase 7 | def test_verify_signature_invalid 8 | VCR.use_cassette("conformance/verify_signature_invalid") do |cassette| 9 | Timecop.freeze(cassette.originally_recorded_at || Time.now) do 10 | capture_output do 11 | e = assert_raise SystemExit do 12 | Sigstore::CLI.start([ 13 | "verify", 14 | "--certificate-identity", "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main", 15 | "--certificate-oidc-issuer", "https://token.actions.githubusercontent.com", 16 | "--bundle", 17 | "test/sigstore-conformance/test/assets/a.txt.invalid_signature.sigstore.json", 18 | "test/sigstore-conformance/test/assets/a.txt" 19 | ]) 20 | end 21 | assert_equal 1, e.status 22 | end 23 | end 24 | end 25 | end 26 | 27 | def test_verify_bundle_success 28 | VCR.use_cassette("conformance/verify_bundle_success") do |cassette| 29 | Timecop.freeze(cassette.originally_recorded_at || Time.now) do 30 | capture_output do 31 | assert_nothing_raised do 32 | Sigstore::CLI.start([ 33 | "verify", 34 | "--bundle", "test/sigstore-conformance/test/assets/a.txt.good.sigstore.json", 35 | "--certificate-identity", "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main", 36 | "--certificate-oidc-issuer", "https://token.actions.githubusercontent.com", 37 | "test/sigstore-conformance/test/assets/a.txt" 38 | ]) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | def test_verify_dsse_bundle_with_trust_root 46 | capture_output do 47 | assert_nothing_raised do 48 | Sigstore::CLI.start([ 49 | "verify", 50 | "--bundle", "test/sigstore-conformance/test/assets/d.txt.good.sigstore.json", 51 | "--certificate-identity", "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main", 52 | "--certificate-oidc-issuer", "https://token.actions.githubusercontent.com", 53 | "--trusted-root", "test/sigstore-conformance/test/assets/trusted_root.d.json", 54 | "--offline", 55 | "test/sigstore-conformance/test/assets/d.txt" 56 | ]) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/sigstore/data/x509/cryptography-scts-tbs-precert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigstore/sigstore-ruby/HEAD/test/sigstore/data/x509/cryptography-scts-tbs-precert.der -------------------------------------------------------------------------------- /test/sigstore/data/x509/cryptography-scts.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGCzCCBPOgAwIBAgISA9MzcqjnMT3tsDXK3Lry4uRIMA0GCSqGSIb3DQEBCwUA 3 | MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD 4 | ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODA5MjYxOTU2MzNaFw0x 5 | ODEyMjUxOTU2MzNaMBoxGDAWBgNVBAMTD2NyeXB0b2dyYXBoeS5pbzCCASIwDQYJ 6 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKJDpCL99DVo83587MrVp6gunmKRoUfY 7 | vcgk5u2v0tB9OmZkcIY37z6AunHWr18Yj55zHmm6G8Nf35hmu3ql2A26WThCbmOe 8 | WXbxhgarkningZI9opUWnI2dIllguVIsq99GzhpNnDdCb26s5+SRhJI4cr4hYaKC 9 | XGDKooKWyXUX09SJTq7nW/1+pq3y9ZMvldRKjJALeAdwnC7kmUB6pK7q8J2VlpfQ 10 | wqGu6q/WHVdgnhWARw3GEFJWDn9wkxBAF08CpzhVaEj+iK+Ut/1HBgNYwqI47h7S 11 | q+qv0G2qklRVUtEM0zYRsp+y/6vivdbFLlPw8VaerbpJN3gLtpVNcGECAwEAAaOC 12 | AxkwggMVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB 13 | BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUjbe0bE1aZ8HiqtwqUfCe15bF 14 | V8UwHwYDVR0jBBgwFoAUqEpqYwR93brm0Tm3pkVl7/Oo7KEwbwYIKwYBBQUHAQEE 15 | YzBhMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcC5pbnQteDMubGV0c2VuY3J5cHQu 16 | b3JnMC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDMubGV0c2VuY3J5cHQu 17 | b3JnLzAaBgNVHREEEzARgg9jcnlwdG9ncmFwaHkuaW8wgf4GA1UdIASB9jCB8zAI 18 | BgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8v 19 | Y3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRp 20 | ZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGll 21 | cyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBv 22 | bGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5 23 | LzCCAQYGCisGAQQB1nkCBAIEgfcEgfQA8gB3ACk8UZZUyDlluqpQ/FgH1Ldvv1h6 24 | KXLcpMMM9OVFR/R4AAABZherSukAAAQDAEgwRgIhAKXOqHxQbnGMJuNIu/QLwQ51 25 | 6E195jqLTR5+iQpy2qRAAiEA3qnx0MNT/NM34VtxX4AohXWAXUt3AsAnAu7Y9xVO 26 | fHIAdwBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAAAWYXq0twAAAE 27 | AwBIMEYCIQCi4Nn+Y5QU+L3N1/adsJDVuJIHtIDHisLFxA42HpKjpgIhALznDcOE 28 | Mfr8hR8lwCNOXN5LkGTgjTx7yttkY+90h2cQMA0GCSqGSIb3DQEBCwUAA4IBAQAq 29 | uKFBh6Rc4kH9yEVNYMu+tv/xCluzCKHvCAmxUF6eF5K9nTFB4gkWjsBCiC+WlCJh 30 | OX53Xg6ZQVCt7dpIk6C2RatNrZt+TnNevDZ04x0+YhMoDUREZay0cZ3h62GsdbQQ 31 | 1UCqDipcNfm1Lr3noCzQJlTejK5cjc/8WozMIxAZorJnIf0kBHXCniRepu5gCmjn 32 | 0WDCj1uLmFr3jRXRiQ8E+ygXh7PclxMHXsVooeZvkBjGoYJCKKirWaDUzw/1bmbG 33 | ZnA/tGyBdUMMyDzd7qcnBYDOIH8UyXo1ySOYyt9sH0OJXBfmxwHkShvD1VbxCmOT 34 | DJbhFMA/rdWmsV8a/ZiD 35 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/sigstore/internal/json_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/internal/json" 5 | 6 | class Sigstore::Internal::JSONTest < Test::Unit::TestCase 7 | def test_canonical_generate 8 | e = assert_raise(ArgumentError) do 9 | Sigstore::Internal::JSON.canonical_generate({ 10 | "foo" => [1.2] 11 | }) 12 | end 13 | assert_equal "Unsupported data type: Float", e.message 14 | 15 | e = assert_raise(ArgumentError) do 16 | Sigstore::Internal::JSON.canonical_generate({ 1 => [] }) 17 | end 18 | assert_equal "Non-string key in hash", e.message 19 | 20 | hash = { 21 | "empty" => "", 22 | "a" => "b", 23 | "null" => nil, 24 | "true" => true, 25 | "false" => false, 26 | "int" => 1, 27 | "array" => [1, 2, 3, "4"], 28 | "empty_hash" => {}, 29 | "empty_array" => [], 30 | "newline_string" => "a\nb\n", 31 | "quote_string" => "a\"b", 32 | "hash" => { "c" => "d", "e" => "f" } 33 | } 34 | assert_equal <<~JSON.chomp, Sigstore::Internal::JSON.canonical_generate(hash) 35 | {"a":"b","array":[1,2,3,"4"],"empty":"","empty_array":[],"empty_hash":{},"false":false,"hash":{"c":"d","e":"f"},"int":1,"newline_string":"a 36 | b 37 | ","null":null,"quote_string":"a\\"b","true":true} 38 | JSON 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/sigstore/internal/keyring_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Sigstore::Internal::KeyringTest < Test::Unit::TestCase 6 | def test_something; end 7 | end 8 | -------------------------------------------------------------------------------- /test/sigstore/internal/merkle_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Sigstore::Internal::MerkleTest < Test::Unit::TestCase 6 | test "verify_merkle_inclusion" do 7 | nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/sigstore/internal/set_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Sigstore::Internal::SETTest < Test::Unit::TestCase 6 | def test_verify_valid; end 7 | def test_verify_invalid; end 8 | end 9 | -------------------------------------------------------------------------------- /test/sigstore/internal/tuf_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/tuf" 5 | 6 | class Sigstore::TUFTest < Test::Unit::TestCase 7 | def test_initialize 8 | updater = Sigstore::TUF::TrustUpdater.new("https://example.com", true) 9 | assert_equal( 10 | File.join( 11 | Dir.home, 12 | ".cache/sigstore-ruby/sigstore/tuf/https%3A%2F%2Fexample.com/trusted_root.json" 13 | ), 14 | updater.trusted_root_path 15 | ) 16 | refute File.file?(updater.trusted_root_path) 17 | end 18 | 19 | def test_production_default_dirs 20 | updater = Sigstore::TUF::TrustUpdater.new("https://tuf-repo-cdn.sigstore.dev", true) 21 | assert_equal( 22 | File.join( 23 | Dir.home, 24 | ".cache/sigstore-ruby/sigstore/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json" 25 | ), 26 | updater.trusted_root_path 27 | ) 28 | 29 | assert File.file?(updater.trusted_root_path) 30 | 31 | assert_equal File.read(updater.trusted_root_path), 32 | File.read(File.expand_path("../../../data/_store/prod/trusted_root.json", __dir__)) 33 | end 34 | 35 | def test_staging_default_dirs 36 | updater = Sigstore::TUF::TrustUpdater.new("https://tuf-repo-cdn.sigstage.dev", true) 37 | assert_equal( 38 | File.join( 39 | Dir.home, 40 | ".cache/sigstore-ruby/sigstore/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstage.dev/trusted_root.json" 41 | ), 42 | updater.trusted_root_path 43 | ) 44 | 45 | assert File.file?(updater.trusted_root_path) 46 | 47 | assert_equal File.read(updater.trusted_root_path), 48 | File.read(File.expand_path("../../../data/_store/staging/trusted_root.json", __dir__)) 49 | end 50 | 51 | def test_initialize_custom_dirs 52 | targets_dir = File.join(Dir.home, "custom-targets") 53 | metadata_dir = File.join(Dir.home, "custom-metadata") 54 | updater = Sigstore::TUF::TrustUpdater.new("https://tuf-repo-cdn.sigstore.dev", true, 55 | metadata_dir:, targets_dir:) 56 | assert_equal(File.join(targets_dir, "trusted_root.json"), updater.trusted_root_path) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/sigstore/internal/x509_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | require "sigstore/internal/x509" 6 | 7 | class Sigstore::Internal::X509::CertificateTest < Test::Unit::TestCase 8 | def test_cert 9 | cert = Sigstore::Internal::X509::Certificate.new( 10 | OpenSSL::X509::Certificate.new(File.binread(File.join(__dir__, "../data/x509/cryptography-scts.pem"))) 11 | ) 12 | refute_nil(cert) 13 | 14 | refute_nil(ext = cert.extension(Sigstore::Internal::X509::Extension::SubjectKeyIdentifier)) 15 | assert_equal("\x8D\xB7\xB4lMZg\xC1\xE2\xAA\xDC*Q\xF0\x9E\xD7\x96\xC5W\xC5".b, ext.key_identifier) 16 | 17 | refute_nil(ext = cert.extension(Sigstore::Internal::X509::Extension::KeyUsage)) 18 | assert_predicate ext, :digital_signature 19 | refute_predicate ext, :key_cert_sign 20 | 21 | refute_nil(ext = cert.extension(Sigstore::Internal::X509::Extension::ExtendedKeyUsage)) 22 | assert_equal(["1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.2"], ext.purposes.map(&:oid)) 23 | 24 | refute_nil(ext = cert.extension(Sigstore::Internal::X509::Extension::BasicConstraints)) 25 | refute_predicate ext, :ca 26 | assert_nil ext.path_len_constraint 27 | 28 | refute_nil(ext = cert.extension(Sigstore::Internal::X509::Extension::PrecertificateSignedCertificateTimestamps)) 29 | assert_equal([ 30 | { entry_type: 1, 31 | extensions_bytes: "", 32 | hash_algorithm: "sha256", 33 | log_id: "293c519654c83965baaa50fc5807d4b76fbf587a2972dca4c30cf4e54547f478", 34 | signature: "0F\x02!\x00\xA5\xCE\xA8|Pnq\x8C&\xE3H\xBB\xF4\v\xC1\x0Eu\xE8M}\xE6:\x8BM\x1E~\x89\n" \ 35 | "r\xDA\xA4@\x02!\x00\xDE\xA9\xF1\xD0\xC3S\xFC\xD37\xE1[q_\x80(\x85u\x80]Kw\x02\xC0'" \ 36 | "\x02\xEE\xD8\xF7\x15N|r".b, 37 | signature_algorithm: "ecdsa", 38 | timestamp: 1_537_995_393_769, 39 | version: 0 } 40 | ], 41 | ext.signed_certificate_timestamps.map(&:to_h)) 42 | end 43 | 44 | def test_tbs_certificate_der 45 | certificate = Sigstore::Internal::X509::Certificate.read( 46 | File.binread(File.join(__dir__, "../data/x509/cryptography-scts.pem")) 47 | ) 48 | expected = File.binread(File.join(__dir__, "../data/x509/cryptography-scts-tbs-precert.der")) 49 | 50 | assert_equal(expected, certificate.tbs_certificate_der) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/sigstore/models_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/models" 5 | require "sigstore/trusted_root" 6 | 7 | class Sigstore::BundleTypeTest < Test::Unit::TestCase 8 | def test_from_media_type 9 | assert_equal(Sigstore::BundleType::BUNDLE_0_1, 10 | Sigstore::BundleType.from_media_type("application/vnd.dev.sigstore.bundle+json;version=0.1")) 11 | assert_equal(Sigstore::BundleType::BUNDLE_0_2, 12 | Sigstore::BundleType.from_media_type("application/vnd.dev.sigstore.bundle+json;version=0.2")) 13 | assert_equal(Sigstore::BundleType::BUNDLE_0_3, 14 | Sigstore::BundleType.from_media_type("application/vnd.dev.sigstore.bundle+json;version=0.3")) 15 | 16 | assert_raise(Sigstore::Error::InvalidBundle) do 17 | Sigstore::BundleType.from_media_type("application/vnd.dev.sigstore.bundle+json;version=0.0") 18 | end 19 | end 20 | 21 | def test_verification_input_no_bundle 22 | verification_input = Sigstore::Verification::V1::Input.new 23 | e = assert_raise(ArgumentError) { Sigstore::VerificationInput.new(verification_input) } 24 | assert_equal("bundle must be a Sigstore::Bundle::V1::Bundle, is NilClass", e.message) 25 | end 26 | 27 | def test_verification_input_bundle_missing_media_type 28 | verification_input = Sigstore::Verification::V1::Input.new 29 | verification_input.bundle = Sigstore::Bundle::V1::Bundle.new 30 | e = assert_raise(Sigstore::Error::InvalidBundle) { Sigstore::VerificationInput.new(verification_input) } 31 | assert_equal("Unsupported bundle format: \"\"", e.message) 32 | end 33 | 34 | def test_verification_input_bundle_missing_verification_material 35 | verification_input = Sigstore::Verification::V1::Input.new 36 | verification_input.bundle = Sigstore::Bundle::V1::Bundle.new 37 | verification_input.bundle.media_type = Sigstore::BundleType::BUNDLE_0_3.media_type 38 | e = assert_raise(Sigstore::Error::InvalidBundle) { Sigstore::VerificationInput.new(verification_input) } 39 | assert_equal("bundle requires verification material", e.message) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/sigstore/oidc_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Sigstore::OIDCTest < Test::Unit::TestCase 6 | def test_something; end 7 | end 8 | -------------------------------------------------------------------------------- /test/sigstore/policy_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Sigstore::PolicyTest < Test::Unit::TestCase 6 | def test_something; end 7 | end 8 | -------------------------------------------------------------------------------- /test/sigstore/rekor/checkpoint_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Sigstore::Rekor::CheckpointTest < Test::Unit::TestCase 6 | def test_something; end 7 | end 8 | -------------------------------------------------------------------------------- /test/sigstore/rekor/client_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Sigstore::Rekor::ClientTest < Test::Unit::TestCase 6 | end 7 | -------------------------------------------------------------------------------- /test/sigstore/transparency_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | require "sigstore/internal/merkle" 6 | 7 | class Sigstore::InclusionProofTest < Test::Unit::TestCase 8 | def test_hasher 9 | [ 10 | # ["RFC6962 Empty", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ], 11 | ["RFC6962 Empty Leaf", "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", 12 | Sigstore::Internal::Merkle.hash_leaf("")], 13 | ["RFC6962 Single Leaf", "395aa064aa4c29f7010acfe3f25db9485bbd4b91897b6ad7ad547639252b4d56", 14 | Sigstore::Internal::Merkle.hash_leaf("L123456")], 15 | ["RFC6962 Node", "aa217fe888e47007fa15edab33c2b492a722cb106c64667fc2b044444de66bbb", 16 | Sigstore::Internal::Merkle.hash_children("N123", "N456")] 17 | ].each do |desc, got, want| 18 | got_hex = [got].pack("H*") 19 | assert_equal got_hex, want, desc 20 | end 21 | end 22 | 23 | def test_hasher_collisions 24 | leaf1 = "Hello" 25 | leaf2 = "World" 26 | 27 | hash1 = Sigstore::Internal::Merkle.hash_leaf(leaf1) 28 | hash2 = Sigstore::Internal::Merkle.hash_leaf(leaf2) 29 | 30 | refute_equal hash1, hash2, "Leaf hashes should differ" 31 | 32 | sub_hash1 = Sigstore::Internal::Merkle.hash_children(hash1, hash2) 33 | preimage = "#{hash1}#{hash2}" 34 | forged_hash = Sigstore::Internal::Merkle.hash_leaf(preimage) 35 | 36 | refute_equal sub_hash1, forged_hash, "Hasher is not second-preimage resistant" 37 | 38 | sub_hash2 = Sigstore::Internal::Merkle.hash_children(hash2, hash1) 39 | 40 | refute_equal sub_hash1, sub_hash2, "Hasher is not order-sensitive" 41 | end 42 | 43 | def test_verify_inclusion_single_entry 44 | data = "data" 45 | # Root and leaf hash for 1-entry tree are the same. 46 | hash = Sigstore::Internal::Merkle.hash_leaf(data) 47 | # The corresponding inclusion proof is empty. 48 | proof = [] 49 | empty_hash = "" 50 | 51 | [ 52 | [hash, hash, false], 53 | [hash, empty_hash, true], 54 | [empty_hash, hash, true], 55 | [empty_hash, empty_hash, true] # wrong hash size 56 | ].each do |root, leaf, want_err| 57 | blk = proc do 58 | Sigstore::Internal::Merkle.verify_inclusion( 59 | 0, 1, proof, root, leaf 60 | ) 61 | end 62 | if want_err 63 | assert_raise(Sigstore::Internal::Merkle::InvalidInclusionProofError, &blk) 64 | else 65 | blk.call 66 | end 67 | end 68 | end 69 | 70 | # dumped from https://github.com/transparency-dev/merkle/blob/main/proof/verify_test.go 71 | File.open(File.expand_path("data/transparency/merkle/verify_inclusion.jsonl", __dir__)) do |f| 72 | f.each_line.map do |l| 73 | c = JSON.parse(l) 74 | 75 | test c["desc"] do 76 | blk = proc { 77 | proof = (c["proof"] || []).map { |h| Sigstore::Internal::Util.base64_decode h } 78 | root = Sigstore::Internal::Util.base64_decode c["root"] 79 | leaf_hash = Sigstore::Internal::Util.base64_decode c["leaf_hash"] 80 | verifier_check( 81 | c["desc"], c["index"], c["size"], proof, root, leaf_hash 82 | ) 83 | } 84 | 85 | if c["expected_error"] 86 | assert_raise(Sigstore::Internal::Merkle::InvalidInclusionProofError, 87 | Sigstore::Internal::Merkle::InclusionProofSizeError, 88 | c.inspect, &blk) 89 | else 90 | assert_nothing_raised(c.inspect, &blk) 91 | end 92 | end 93 | end 94 | end 95 | 96 | def verifier_check(desc, log_index, tree_size, proof, root, leaf_hash) 97 | got = Sigstore::Internal::Merkle.root_from_inclusion_proof( 98 | log_index, tree_size, proof, leaf_hash 99 | ) 100 | 101 | Sigstore::Internal::Merkle.verify_inclusion( 102 | log_index, tree_size, proof, root, leaf_hash 103 | ) 104 | 105 | assert_equal Sigstore::Internal::Util.base64_encode(root), Sigstore::Internal::Util.base64_encode(got), 106 | "#{desc}: got root #{got.inspect}, want #{root.inspect}" 107 | assert_equal root, got, "#{desc}: got root #{got.inspect}, want #{root.inspect}" 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/sigstore/trusted_root_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/trusted_root" 5 | 6 | class Sigstore::TrustedRootTest < Test::Unit::TestCase 7 | def test_production 8 | VCR.use_cassette("production") do |cassette| 9 | Timecop.freeze(cassette.originally_recorded_at || Time.now) do 10 | production = Sigstore::TrustedRoot.production 11 | assert_equal "application/vnd.dev.sigstore.trustedroot+json;version=0.1", production.media_type 12 | assert_equal ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9f\n" \ 13 | "AFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RY\n" \ 14 | "tw==\n"], production.rekor_keys.map { [_1.to_der].pack("m") } 15 | assert_equal ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy\n" \ 16 | "3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU\n" \ 17 | "7w==\n", 18 | "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEc\n" \ 19 | "YXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9F\n" \ 20 | "yw==\n"], production.ctfe_keys.map { [_1.to_der].pack("m") } 21 | assert_equal "chain 0\n" \ 22 | "-----BEGIN CERTIFICATE-----\n" \ 23 | "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq\n" \ 24 | "MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx\n" \ 25 | "MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu\n" \ 26 | "ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy\n" \ 27 | "A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas\n" \ 28 | "taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm\n" \ 29 | "MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE\n" \ 30 | "FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u\n" \ 31 | "Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx\n" \ 32 | "Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup\n" \ 33 | "Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\n" \ 34 | "-----END CERTIFICATE-----\n" \ 35 | "chain 1\n" \ 36 | "-----BEGIN CERTIFICATE-----\n" \ 37 | "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\n" \ 38 | "KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\n" \ 39 | "MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\n" \ 40 | "LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C\n" \ 41 | "AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7\n" \ 42 | "7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS\n" \ 43 | "0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB\n" \ 44 | "BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp\n" \ 45 | "KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI\n" \ 46 | "zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR\n" \ 47 | "nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP\n" \ 48 | "mygUY7Ii2zbdCdliiow=\n" \ 49 | "-----END CERTIFICATE-----\n" \ 50 | "-----BEGIN CERTIFICATE-----\n" \ 51 | "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw\n" \ 52 | "KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\n" \ 53 | "MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl\n" \ 54 | "LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7\n" \ 55 | "XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex\n" \ 56 | "X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j\n" \ 57 | "YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY\n" \ 58 | "wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ\n" \ 59 | "KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\n" \ 60 | "WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\n" \ 61 | "TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n" \ 62 | "-----END CERTIFICATE-----\n", production.fulcio_cert_chains.map.with_index { |chain, i| 63 | "chain #{i}\n" + chain.map(&:to_pem).join 64 | }.join 65 | end 66 | end 67 | end 68 | 69 | def test_production_offline 70 | production_offline = Sigstore::TrustedRoot.production(offline: true) 71 | assert_equal "application/vnd.dev.sigstore.trustedroot+json;version=0.1", production_offline.media_type 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/sigstore/tuf/root_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | require "sigstore/tuf/root" 6 | 7 | class Sigstore::RootTest < Test::Unit::TestCase 8 | def test_something; end 9 | end 10 | -------------------------------------------------------------------------------- /test/sigstore/tuf/snapshot_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/tuf/snapshot" 5 | 6 | class Sigstore::TUF::SnapshotTest < Test::Unit::TestCase 7 | def test_something; end 8 | end 9 | -------------------------------------------------------------------------------- /test/sigstore/tuf/targets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/tuf/targets" 5 | 6 | class Sigstore::TUF::TargetsTest < Test::Unit::TestCase 7 | def test_something; end 8 | end 9 | -------------------------------------------------------------------------------- /test/sigstore/tuf/timestamp_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/tuf/timestamp" 5 | 6 | class Sigstore::TUF::TimestampTest < Test::Unit::TestCase 7 | def test_something; end 8 | end 9 | -------------------------------------------------------------------------------- /test/sigstore/tuf/trusted_metadata_set_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/tuf/trusted_metadata_set" 5 | 6 | class Sigstore::TUF::TrustedMetadataSetTest < Test::Unit::TestCase 7 | setup do 8 | @reference_time = Time.utc(12, 42, 2, 7, 3, 2023, 4, 67, false, "UTC") 9 | @priv_key = OpenSSL::PKey::RSA.new(2048) 10 | @root = { 11 | "signed" => { 12 | "_type" => "root", 13 | "spec_version" => "1.0.0", 14 | "consistent_snapshot" => false, 15 | "version" => 1, 16 | "expires" => "2023-07-03T12:42:02Z", 17 | "keys" => {}, 18 | "roles" => { 19 | "root" => { 20 | "keyids" => [], 21 | "threshold" => 0 22 | }, 23 | "timestamp" => { 24 | "keyids" => [], 25 | "threshold" => 0 26 | }, 27 | "snapshot" => { 28 | "keyids" => [], 29 | "threshold" => 0 30 | } 31 | } 32 | }, 33 | "signatures" => [] 34 | } 35 | @timestamp = { 36 | "signed" => { 37 | "_type" => "timestamp", 38 | "version" => 1, 39 | "spec_version" => "1.0.0", 40 | "expires" => "2023-07-03T12:42:02Z", 41 | "meta" => { 42 | "snapshot.json" => { 43 | "version" => 137, 44 | "length" => 104, 45 | "hashes" => { 46 | "sha256" => "5c4853e87c01a1621f410ad55f80bedb6c5b7e55c1f6e59d739769c0b54cf558" 47 | } 48 | } 49 | } 50 | }, 51 | "signatures" => [] 52 | } 53 | @snapshot = { 54 | "signed" => { 55 | "_type" => "snapshot", 56 | "version" => 137, 57 | "expires" => "2023-07-03T12:42:02Z", 58 | "meta" => {} 59 | }, 60 | "signatures" => [] 61 | } 62 | # TODO: need to generate a test root, with keys we can use for signing 63 | @root_data = JSON.dump(@root) 64 | @set = Sigstore::TUF::TrustedMetadataSet.new(@root_data, "json", reference_time: @reference_time) 65 | end 66 | 67 | def test_initialize_known_good 68 | @root_data = File.read(File.expand_path("../../../data/_store/prod/root.json", __dir__)) 69 | Sigstore::TUF::TrustedMetadataSet.new(@root_data, "json", reference_time: @reference_time) 70 | end 71 | 72 | def test_initialize 73 | assert @set.root 74 | 75 | # allows loading expired metadata from the (already truted) root 76 | @reference_time += 60 * 60 * 24 * 365 * 20 77 | Sigstore::TUF::TrustedMetadataSet.new(@root_data, "json", reference_time: @reference_time) 78 | end 79 | 80 | def test_raises_when_updating_root_after_timestamp 81 | @set.timestamp = JSON.dump(@timestamp) 82 | e = assert_raise(Sigstore::TUF::Error::BadUpdateOrder) do 83 | @set.root = @root_data 84 | end 85 | 86 | assert_equal "cannot update root after timestamp", e.message 87 | end 88 | 89 | def test_raises_when_updating_snapshot_before_timestamp 90 | e = assert_raise(Sigstore::TUF::Error::BadUpdateOrder) do 91 | @set.snapshot = JSON.dump(@snapshot) 92 | end 93 | 94 | assert_equal "cannot update snapshot before timestamp", e.message 95 | end 96 | 97 | def test_raises_when_updating_timestamp_after_snapshot 98 | @set.timestamp = JSON.dump(@timestamp) 99 | @set.snapshot = JSON.dump(@snapshot) 100 | e = assert_raise(Sigstore::TUF::Error::BadUpdateOrder) do 101 | @set.timestamp = JSON.dump(@timestamp) 102 | end 103 | 104 | assert_equal "cannot update timestamp after snapshot", e.message 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /test/sigstore/verifier_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/verifier" 5 | 6 | class Sigstore::VerifierTest < Test::Unit::TestCase 7 | def test_pack_digitally_signed_precertificate 8 | verifier = Sigstore::Verifier.allocate 9 | [3, 255, 1024, 16_777_215].each do |precert_bytes_len| 10 | precert_bytes = "x".b * precert_bytes_len 11 | sct = Sigstore::Internal::X509::Extension::PrecertificateSignedCertificateTimestamps::Timestamp.new( 12 | log_id: nil, 13 | extensions_bytes: nil, 14 | hash_algorithm: nil, 15 | signature_algorithm: nil, 16 | signature: nil, 17 | 18 | version: 0, 19 | timestamp: 1234, 20 | entry_type: 1 21 | ) 22 | issuer_key_id = "iamapublickeyshatwofivesixdigest" 23 | cert = Sigstore::Internal::X509::Certificate.allocate 24 | cert.singleton_class.send(:define_method, :tbs_certificate_der) { precert_bytes } 25 | data = verifier.send(:pack_digitally_signed, sct, cert, issuer_key_id) 26 | _, l1, l2, l3 = [precert_bytes.bytesize].pack("N").unpack("C4") 27 | assert_equal [ 28 | "\x00", # version 29 | "\x00", # signature_type 30 | "\x00\x00\x00\x00\x00\x00\x04\xD2", # timestamp 31 | "\x00\x01", # entry_type 32 | issuer_key_id, 33 | l1.chr, l2.chr, l3.chr, # tbs cert len 34 | precert_bytes, 35 | "\x00\x00", # extensions length 36 | "" # extensions 37 | ].map!(&:b).join, data, "precert_bytes_len=#{precert_bytes_len}" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/sigstore/version_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "sigstore/version" 5 | 6 | class Sigstore::VersionTest < Test::Unit::TestCase 7 | def test_version 8 | assert_instance_of String, Sigstore::VERSION 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | SimpleCov.configure do 5 | add_filter "test/" 6 | end 7 | SimpleCov.start if ENV["COVERAGE"] 8 | 9 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 10 | require "sigstore" 11 | 12 | require "test-unit" 13 | require "webmock/test_unit" 14 | require "vcr" 15 | require "json" 16 | require "timecop" 17 | require "tmpdir" 18 | 19 | WebMock.disable_net_connect! 20 | 21 | VCR.configure do |config| 22 | config.cassette_library_dir = "fixtures/vcr_cassettes" 23 | config.hook_into :webmock 24 | config.default_cassette_options = { record: :new_episodes } 25 | end 26 | 27 | class Test::Unit::TestCase 28 | setup 29 | def env_home_setup 30 | @xdg_data_home = ENV.fetch("XDG_DATA_HOME", nil) 31 | @xdg_cache_home = ENV.fetch("XDG_CACHE_HOME", nil) 32 | @home = ENV.fetch("HOME", nil) # rubocop:disable Style/EnvHome 33 | @tmp_home = Dir.mktmpdir 34 | 35 | ENV.update( 36 | "XDG_DATA_HOME" => nil, 37 | "XDG_CACHE_HOME" => nil, 38 | "HOME" => @tmp_home 39 | ) 40 | end 41 | 42 | cleanup 43 | def env_home_cleanup 44 | FileUtils.remove_entry_secure(@tmp_home) 45 | ENV.update( 46 | "XDG_DATA_HOME" => @xdg_data_home, 47 | "XDG_CACHE_HOME" => @xdg_cache_home, 48 | "HOME" => @home 49 | ) 50 | end 51 | end 52 | --------------------------------------------------------------------------------