Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,24 @@ jobs:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true

# The xfail set depends on the OpenSSL build's behavior (ruby/openssl#770), so it is
# computed in the Rakefile (conformance_xfails) and shared with local `rake
# conformance` runs rather than duplicated as a Ruby-version guess here.
- name: Compute the conformance xfails for this Ruby/OpenSSL build
id: conformance_xfails
run: echo "xfail=$(bundle exec rake conformance_xfails)" >> "$GITHUB_OUTPUT"

- name: Run the conformance tests
uses: sigstore/sigstore-conformance@d658ea74a060aeabae78f8a379167f219dc38c38 # v0.0.16
uses: sigstore/sigstore-conformance@21533cde107c734ebc153c3e3a24d75fc9811a36 # v0.0.29
with:
entrypoint: ${{ github.workspace }}/bin/conformance-entrypoint
xfail: "${{ matrix.ruby != 'head' && matrix.ruby != 'truffleruby-head' && matrix.ruby != '3.4' && matrix.ruby != '4.0' && 'test_verify_rejects_bad_tsa_timestamp' }}"
xfail: ${{ steps.conformance_xfails.outputs.xfail }}
if: ${{ matrix.os }} == "ubuntu-latest"
- name: Run the conformance tests against staging
uses: sigstore/sigstore-conformance@d658ea74a060aeabae78f8a379167f219dc38c38 # v0.0.16
uses: sigstore/sigstore-conformance@21533cde107c734ebc153c3e3a24d75fc9811a36 # v0.0.29
with:
entrypoint: ${{ github.workspace }}/bin/conformance-entrypoint
xfail: "${{ matrix.ruby != 'head' && matrix.ruby != 'truffleruby-head' && matrix.ruby != '3.4' && matrix.ruby != '4.0' && 'test_verify_rejects_bad_tsa_timestamp' }}"
xfail: ${{ steps.conformance_xfails.outputs.xfail }}
environment: staging
if: ${{ matrix.os }} == "ubuntu-latest"

Expand Down Expand Up @@ -135,7 +142,7 @@ jobs:
run: bin/rake bin/tuf-conformance-entrypoint.xfails

- name: Run the TUF conformance tests
uses: theupdateframework/tuf-conformance@9bfc222a371e30ad5511eb17449f68f855fb9d8f # v2.3.0
uses: theupdateframework/tuf-conformance@500c525c9ce287a472fd334fe8d885cace667d32 # v2.4.0
with:
entrypoint: ${{ github.workspace }}/bin/tuf-conformance-entrypoint
artifact-name: "test repositories ${{ matrix.ruby }} ${{ matrix.os }}"
Expand Down Expand Up @@ -172,15 +179,21 @@ jobs:
id: list-gems
run: |
echo "gems=$(find pkg -type f -name '*.gem' -print0 | xargs -0 jq --compact-output --null-input --args '[$ARGS.positional[]]')" >> $GITHUB_OUTPUT
# The smoketest job runs with no id-token permission (so it works on fork PRs), so it
# can't use ambient GitHub OIDC. It signs with sigstore-conformance's public test token
# instead. This replaces the deprecated extremely-dangerous-public-oidc-beacon action;
# the token now comes from a Google service account (issuer https://accounts.google.com).
# (The release workflow signs with the real ambient GitHub identity, not this token.)
- name: Fetch testing OIDC token
uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@4a8befcc16064dac9e97f210948d226e5c869bdc # v1.0.0
run: curl -sSfL --retry 3 https://storage.googleapis.com/sigstore-conformance-testing-token/untrusted-testing-token.txt -o oidc-token.txt
- name: Run the smoketest
run: |
./bin/smoketest ${BUILT_GEMS}
env:
BUILT_GEMS: ${{ join(fromJson(steps.list-gems.outputs.gems), ' ') }}
OIDC_TOKEN_FILE: ./oidc-token.txt
SIGSTORE_CERT_IDENTITY: https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main
SIGSTORE_CERT_IDENTITY: untrusted-sa@sigstore-conformance.iam.gserviceaccount.com
SIGSTORE_CERT_OIDC_ISSUER: https://accounts.google.com

all-tests-pass:
if: always()
Expand Down
63 changes: 63 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,66 @@
## [0.3.0] - 2026-06-20

### Added

- Rekor v2 (tiled transparency log) verification and signing, using the
`hashedrekord` 0.0.2 entry type.
- DSSE / in-toto attestation signing (`Signer#sign_dsse`).
- Managed-key (bring-your-own-key) verification via `Verifier#verify(key:)`,
for bundles that carry a public-key hint instead of a Fulcio certificate.
- Timestamping Authority (TSA / RFC 3161) timestamp signing, and defaulting the
signing configuration from TUF.

### Changed

- Updated to sigstore protobuf-specs v0.5.1 / protobug 0.2.0, and added the
`protobug_in_toto_attestation_protos` dependency. The `protobug_sigstore_protos`
constraint is now `~> 0.2.0`.
- Updated conformance suites to sigstore-conformance v0.0.29 and
tuf-conformance v2.4.0.

### Security

- Enforce that an RFC 3161 timestamp's message imprint covers the bundle
signature, and that the timestamp's `gen_time` falls within the Timestamping
Authority's validity window. Verification now fails closed when no trusted
signing time (TSA response or log integrated time) is available.
- A managed-key DSSE bundle carried as a Rekor v1 entry now fails with a clear
error instead of raising `NoMethodError`.

## [0.2.3] - 2026-03-10

### Security

- Fix in-toto statement verification (GHSA-mhg6-2q2v-9h2c).

### Changed

- Accept extensions for SCTs.
- Set a library-specific `User-Agent` header on outbound HTTP requests.

## [0.2.2] - 2025-10-24

### Changed

- Require Ruby >= 3.2.
- Re-implement missing JRuby functionality atop `java.security`.
- Ensure `kind_version` is set on transparency log entries after signing.
- Skip unrecognized keys when parsing.
- Enable smoke tests for fork PRs using the public OIDC beacon.

## [0.2.1] - 2024-11-19

- Fix the release automation (gem push paths; split the RubyGems release to a
matrix).

## [0.2.0] - 2024-11-18

### Changed

- Extract the CLI into a separate gem that can be published independently.
- Improve compatibility with the sigstore-js mock server.
- Improve error handling.

## [0.1.1] - 2024-10-18

- Fix release automation
Expand Down
47 changes: 26 additions & 21 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
PATH
remote: .
specs:
sigstore (0.2.3)
sigstore (0.3.0)
logger
net-http
protobug_sigstore_protos (~> 0.1.0)
protobug_in_toto_attestation_protos (~> 0.2.0)
protobug_sigstore_protos (~> 0.2.0)
uri

PATH
remote: cli
specs:
sigstore-cli (0.2.3)
sigstore (= 0.2.3)
sigstore-cli (0.3.0)
sigstore (= 0.3.0)
thor

GEM
Expand Down Expand Up @@ -48,16 +49,19 @@ GEM
racc
power_assert (2.0.5)
prism (1.9.0)
protobug (0.1.0)
protobug_googleapis_field_behavior_protos (0.1.0)
protobug (= 0.1.0)
protobug_well_known_protos (= 0.1.0)
protobug_sigstore_protos (0.1.0)
protobug (= 0.1.0)
protobug_googleapis_field_behavior_protos (= 0.1.0)
protobug_well_known_protos (= 0.1.0)
protobug_well_known_protos (0.1.0)
protobug (= 0.1.0)
protobug (0.2.0)
protobug_googleapis_field_behavior_protos (0.2.0)
protobug (= 0.2.0)
protobug_well_known_protos (= 0.2.0)
protobug_in_toto_attestation_protos (0.2.0)
protobug (= 0.2.0)
protobug_well_known_protos (= 0.2.0)
protobug_sigstore_protos (0.2.0)
protobug (= 0.2.0)
protobug_googleapis_field_behavior_protos (= 0.2.0)
protobug_well_known_protos (= 0.2.0)
protobug_well_known_protos (0.2.0)
protobug (= 0.2.0)
public_suffix (6.0.1)
racc (1.8.1)
racc (1.8.1-java)
Expand Down Expand Up @@ -153,10 +157,11 @@ CHECKSUMS
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
power_assert (2.0.5) sha256=63b511b85bb8ea57336d25156864498644f5bbf028699ceda27949e0125bc323
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
protobug (0.1.0) sha256=5bf1356cedf99dcf311890743b78f5e602f62ca703e574764337f1996b746bf2
protobug_googleapis_field_behavior_protos (0.1.0) sha256=db48ef6a5913b2355b4a6931ab400a9e3e995fb48499977a3ad0be6365f9e265
protobug_sigstore_protos (0.1.0) sha256=4ad1eebaf6454131b6f432dda50ad0e513773613474b92470847614a5acacce1
protobug_well_known_protos (0.1.0) sha256=356757f562453bb34a28f12e8e9fa357346cca35a6807a549837c3fe256bb5b3
protobug (0.2.0) sha256=eb154bdbe2a3afe796711e0c16e89b2ed59efee0f172a10b634400471299b3f2
protobug_googleapis_field_behavior_protos (0.2.0) sha256=5d4ddcdcfd8616a74ac100de893e3768feee631579a777e23498d3a0ba45b893
protobug_in_toto_attestation_protos (0.2.0) sha256=64317c2bb5efe8494ecd0da6990f3c027e7af58bf12c34d6dbd1aa43fd87f731
protobug_sigstore_protos (0.2.0) sha256=b094cbb2ceadc9ffe6e34ec217613dd7a8e78b6411bf4404bd686f7343514e25
protobug_well_known_protos (0.2.0) sha256=89dd4965ed80ed9355b575e9243e7762487de581a8e2b34a97af6bef60fe3b3f
public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
racc (1.8.1-java) sha256=54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98
Expand All @@ -169,8 +174,8 @@ CHECKSUMS
rubocop-performance (1.23.1) sha256=f22f86a795f5e6a6180aac2c6fc172534b173a068d6ed3396d6460523e051b82
rubocop-rake (0.6.0) sha256=56b6f22189af4b33d4f4e490a555c09f1281b02f4d48c3a61f6e8fe5f401d8db
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
sigstore (0.2.3)
sigstore-cli (0.2.3)
sigstore (0.3.0)
sigstore-cli (0.3.0)
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
simplecov-html (0.12.3) sha256=4b1aad33259ffba8b29c6876c12db70e5750cb9df829486e4c6e5da4fa0aa07b
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
Expand All @@ -183,4 +188,4 @@ CHECKSUMS
webmock (3.25.1) sha256=ab9d5d9353bcbe6322c83e1c60a7103988efc7b67cd72ffb9012629c3d396323

BUNDLED WITH
2.6.9
4.0.14
54 changes: 43 additions & 11 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "bundler/gem_tasks"
require "rake/testtask"
require "openssl"

directory "pkg"
namespace "cli" do
Expand All @@ -24,8 +25,39 @@ RuboCop::RakeTask.new
task default: %i[test conformance_staging conformance conformance_tuf rubocop]

require "openssl"
# Checks for https://github.com/ruby/openssl/pull/770
xfail = OpenSSL::X509::Store.new.instance_variable_defined?(:@time) ? "test_verify_rejects_bad_tsa_timestamp" : ""
# On OpenSSL builds with a broken X509::Store#time (ruby/openssl#770) RFC 3161 timestamps
# cannot be verified. Rekor v2 (tiled) entries have no integrated time, so every positive
# v2 case — and the v2 sign+verify roundtrip — fails closed there, and the negative TSA
# cases that depend on the timestamp check no longer reject. Patched builds verify them,
# so these xfails are conditional on the actual (broken) behavior rather than on the Ruby
# version. Negative rekor2 *_fail cases are intentionally absent: they still reject (for
# lack of trusted time) and would XPASS under pytest's strict xfail.
xfail =
if OpenSSL::X509::Store.new.instance_variable_defined?(:@time)
%w[
test_verify_rejects_bad_tsa_timestamp
*rekor2-happy-path*
*rekor2-dsse-happy-path*
*rekor2-checkpoint-cosigned*
*rekor2-checkpoint-two-sigs-cosigned*
*rekor2-checkpoint-multiple-cosigs*
*rekor2-checkpoint-origin-not-first*
*rekor2-checkpoint-two-sigs-from-origin*
*rekor2-timestamp-with-embedded-cert*
*rekor2-timestamp-with-expired-cert-chain*
*rekor2-timestamp-without-embedded-cert*
*intoto-tsa-timestamp-outside-cert-validity_fail*
*bundle-with-sct-with-extensions*
test_sign_verify_rekor2
].join(" ")
else
""
end

desc "Print the conformance xfail patterns for the current Ruby/OpenSSL build"
task :conformance_xfails do
print xfail
end

desc "Run the conformance tests"
task conformance: %w[conformance:setup] do
Expand Down Expand Up @@ -59,7 +91,8 @@ end

task :find_action_versions do # rubocop:disable Rake/Desc
require "yaml"
gh = YAML.load_file(".github/workflows/ci.yml")
# Enable aliases in case the workflow uses YAML anchors.
gh = YAML.load_file(".github/workflows/ci.yml", aliases: true)
actions = gh.fetch("jobs").flat_map { |_, job| job.fetch("steps", []).filter_map { |step| step.fetch("uses", nil) } }
.uniq.map { |x| x.split("@", 2) }
.group_by(&:first).transform_values { |v| v.map(&:last) }
Expand Down Expand Up @@ -170,14 +203,13 @@ end

namespace :tuf_conformance do
file "bin/tuf-conformance-entrypoint.xfails" do |t|
if RUBY_ENGINE == "jruby"
File.write(t.name, <<~TXT)
test_keytype_and_scheme[rsa/rsassa-pss-sha256]
test_keytype_and_scheme[ed25519/ed25519]
TXT
else
File.write(t.name, "")
end
# RSASSA-PSS verification requires OpenSSL::PKey::RSA#verify_pss (see
# internal/key.rb); without it the rsa/rsassa-pss-sha256 key type xfails.
# Gate on the capability rather than the engine: older jruby-openssl lacks
# the method (stable jruby), while jruby-head and CRuby have it, so this
# self-corrects and avoids pytest's strict xfail turning a pass into a failure.
xfails = OpenSSL::PKey::RSA.method_defined?(:verify_pss) ? "" : "test_keytype_and_scheme[rsa/rsassa-pss-sha256]\n"
File.write(t.name, xfails)
end
file "test/tuf-conformance/env/pyvenv.cfg" => :tuf_conformance do
sh "make", "dev", chdir: "test/tuf-conformance"
Expand Down
15 changes: 11 additions & 4 deletions bin/smoketest
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ include FileUtils # rubocop:disable Style/MixinUsage

raise(StandardError, "Usage: #{$PROGRAM_NAME} <dists...>") if ARGV.empty?

dists = ARGV
# Install the base library gem before any gem that depends on it. sigstore-cli pins
# an exact sigstore version that is not yet published when smoketesting a release, so
# `gem install sigstore-cli-x.y.z.gem` would otherwise fail to resolve the dependency
# unless the local sigstore-x.y.z.gem is installed first.
dists = ARGV.sort_by { |d| File.basename(d).start_with?("sigstore-cli") ? 1 : 0 }
mkdir_p %w[smoketest-gem-home smoketest-artifacts]

at_exit { rm_rf "smoketest-gem-home" }
Expand All @@ -22,8 +26,11 @@ env = {
"BUNDLE_GEMFILE" => "smoketest-gem-home/Gemfile"
}

# Get cert identity from environment
# Get the expected signing identity and its OIDC issuer from the environment. The issuer
# defaults to GitHub Actions (the ambient identity used when releasing); CI overrides it
# when signing with a test token from a different issuer.
cert_identity = ENV.fetch("SIGSTORE_CERT_IDENTITY")
cert_oidc_issuer = ENV.fetch("SIGSTORE_CERT_OIDC_ISSUER", "https://token.actions.githubusercontent.com")

# Read OIDC token from file if available
oidc_token_file = ENV.fetch("OIDC_TOKEN_FILE", nil)
Expand Down Expand Up @@ -57,14 +64,14 @@ dists.each do |dist|
"verify",
"--signature=smoketest-artifacts/#{File.basename(dist)}.sig",
"--certificate=smoketest-artifacts/#{File.basename(dist)}.crt",
"--certificate-oidc-issuer=https://token.actions.githubusercontent.com",
"--certificate-oidc-issuer=#{cert_oidc_issuer}",
"--certificate-identity=#{cert_identity}",
dist,
exception: true)
sh(env, File.expand_path("sigstore-cli", __dir__),
"verify",
"--bundle=smoketest-artifacts/#{File.basename(dist)}.sigstore.json",
"--certificate-oidc-issuer=https://token.actions.githubusercontent.com",
"--certificate-oidc-issuer=#{cert_oidc_issuer}",
"--certificate-identity=#{cert_identity}",
dist,
exception: true)
Expand Down
Loading
Loading