sigstore 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/data/_store/prod/root.json +29 -53
- data/data/_store/prod/trusted_root.json +25 -13
- data/data/_store/staging/root.json +4 -4
- data/data/_store/staging/trusted_root.json +155 -7
- data/lib/sigstore/error.rb +4 -0
- data/lib/sigstore/internal/key.rb +83 -18
- data/lib/sigstore/internal/util.rb +7 -0
- data/lib/sigstore/internal/x509.rb +6 -1
- data/lib/sigstore/models.rb +55 -15
- data/lib/sigstore/oidc.rb +3 -0
- data/lib/sigstore/policy.rb +8 -0
- data/lib/sigstore/rekor/checkpoint.rb +17 -7
- data/lib/sigstore/rekor/client.rb +33 -0
- data/lib/sigstore/signer.rb +246 -35
- data/lib/sigstore/signing_config.rb +181 -0
- data/lib/sigstore/trusted_root.rb +20 -4
- data/lib/sigstore/tuf.rb +13 -0
- data/lib/sigstore/verifier.rb +254 -57
- data/lib/sigstore/version.rb +1 -1
- data/lib/sigstore.rb +1 -0
- metadata +19 -4
data/lib/sigstore/verifier.rb
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
17
|
require_relative "trusted_root"
|
|
18
|
+
require_relative "policy"
|
|
18
19
|
require_relative "internal/keyring"
|
|
19
20
|
require_relative "internal/merkle"
|
|
20
21
|
require_relative "internal/set"
|
|
@@ -54,20 +55,36 @@ module Sigstore
|
|
|
54
55
|
for_trust_root(trust_root:)
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
# +key+ is an out-of-band public key (an OpenSSL::PKey) used to verify "managed"
|
|
59
|
+
# (bring-your-own-key) bundles, which carry a public key hint instead of a Fulcio
|
|
60
|
+
# certificate. It must be supplied for, and only for, such bundles.
|
|
61
|
+
def verify(input:, policy:, offline:, key: nil)
|
|
58
62
|
# First, establish a time for the signature. This timestamp is required to validate the certificate chain,
|
|
59
63
|
# so this step comes first.
|
|
60
64
|
|
|
61
65
|
bundle = input.sbundle
|
|
62
66
|
materials = bundle.verification_material
|
|
63
67
|
|
|
68
|
+
if bundle.key_based?
|
|
69
|
+
unless key
|
|
70
|
+
return VerificationFailure.new("bundle is signed with a managed key but no verifying key was provided")
|
|
71
|
+
end
|
|
72
|
+
elsif key
|
|
73
|
+
return VerificationFailure.new("a verifying key was provided but the bundle contains a signing certificate")
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
# 1)
|
|
65
77
|
# If the verification policy uses the Timestamping Service, the Verifier MUST verify the timestamping response
|
|
66
78
|
# using the Timestamping Service root key material, as described in Spec: Timestamping Service, with the raw bytes
|
|
67
79
|
# of the signature as the timestamped data. The Verifier MUST then extract a timestamp from the timestamping
|
|
68
80
|
# response. If verification or timestamp parsing fails, the Verifier MUST abort.
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
# Only resolve the data the timestamp binds to (and enforce its constraints) when
|
|
83
|
+
# there is a timestamp to verify; absent timestamp data there is nothing to bind.
|
|
84
|
+
timestamp_data = materials.timestamp_verification_data
|
|
85
|
+
timestamps = extract_timestamp_from_verification_data(
|
|
86
|
+
timestamp_data, timestamp_data && timestamped_data(bundle)
|
|
87
|
+
) || []
|
|
71
88
|
|
|
72
89
|
# 2)
|
|
73
90
|
# If the verification policy uses timestamps from the Transparency Service, the Verifier MUST verify the signature
|
|
@@ -79,7 +96,7 @@ module Sigstore
|
|
|
79
96
|
begin
|
|
80
97
|
# TODO: should this instead be an input to the verify method?
|
|
81
98
|
# See https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit?disco=AAABQVV-gT0
|
|
82
|
-
entry = find_rekor_entry(bundle, input.hashed_input, offline:)
|
|
99
|
+
entry = find_rekor_entry(bundle, input.hashed_input, offline:, signing_key: key)
|
|
83
100
|
rescue Sigstore::Error::MissingRekorEntry
|
|
84
101
|
return VerificationFailure.new("Rekor entry not found")
|
|
85
102
|
else
|
|
@@ -95,7 +112,16 @@ module Sigstore
|
|
|
95
112
|
|
|
96
113
|
Internal::SET.verify_set(keyring: @rekor_keyring, entry:) if entry.inclusion_promise
|
|
97
114
|
|
|
98
|
-
|
|
115
|
+
# Rekor v1 entries carry an integrated time signed by the log; Rekor v2 (tiled)
|
|
116
|
+
# entries do not, and rely on a Timestamping Service response for the signing time.
|
|
117
|
+
integrated_time = entry.integrated_time
|
|
118
|
+
timestamps << Time.at(integrated_time).utc if integrated_time&.positive?
|
|
119
|
+
|
|
120
|
+
if timestamps.empty?
|
|
121
|
+
return VerificationFailure.new(
|
|
122
|
+
"no trusted signing time available (no timestamp authority response and no log integrated time)"
|
|
123
|
+
)
|
|
124
|
+
end
|
|
99
125
|
|
|
100
126
|
# 3)
|
|
101
127
|
# The Verifier MUST perform certification path validation (RFC 5280 §6) of the certificate chain with the
|
|
@@ -104,44 +130,60 @@ module Sigstore
|
|
|
104
130
|
# timestamp from the Timestamping Service. If a timestamp from the Transparency Service is available, the Verifier
|
|
105
131
|
# MUST perform path validation using the timestamp from the Transparency Service. If both are available, the
|
|
106
132
|
# Verifier performs path validation twice. If either fails, verification fails.
|
|
133
|
+
#
|
|
134
|
+
# Steps 3-5 are certificate-specific: a managed-key bundle has no certificate to do
|
|
135
|
+
# path validation, SCT verification, or identity-policy checks against, so they are
|
|
136
|
+
# skipped. The signature (and its binding to the Rekor entry) is still verified below
|
|
137
|
+
# against the supplied key.
|
|
107
138
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
139
|
+
if bundle.key_based? && !policy.is_a?(Policy::UnsafeNoOp)
|
|
140
|
+
logger.warn do
|
|
141
|
+
"ignoring identity policy #{policy.class} for managed-key bundle: managed-key " \
|
|
142
|
+
"verification trusts the supplied key, not a certificate identity"
|
|
143
|
+
end
|
|
113
144
|
end
|
|
114
145
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
chain.map(&:to_text).join("\n")
|
|
120
|
-
end.join("\n\n")
|
|
121
|
-
end
|
|
146
|
+
unless bundle.key_based?
|
|
147
|
+
chains = timestamps.map do |ts|
|
|
148
|
+
chain, err = Internal::X509.validate_chain(@fulcio_cert_chains, bundle.leaf_certificate, ts)
|
|
149
|
+
return err if err
|
|
122
150
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# SignedCertificateTimestamp embedded in the leaf certificate, and verify it as in RFC 9162 §8.1.3,
|
|
126
|
-
# using the verification key from the Certificate Transparency Log.
|
|
127
|
-
chain = chains.first
|
|
128
|
-
if (result = verify_scts(bundle.leaf_certificate, chain)) && !result.verified?
|
|
129
|
-
return result
|
|
130
|
-
end
|
|
151
|
+
chain
|
|
152
|
+
end
|
|
131
153
|
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
chains.uniq! { |chain| chain.map(&:to_der) }
|
|
155
|
+
unless chains.size == 1
|
|
156
|
+
raise "expected exactly one certificate chain, got #{chains.size} chains:\n" +
|
|
157
|
+
chains.map do |chain|
|
|
158
|
+
chain.map(&:to_text).join("\n")
|
|
159
|
+
end.join("\n\n")
|
|
160
|
+
end
|
|
134
161
|
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
# 4)
|
|
163
|
+
# Unless performing online verification (see §Alternative Workflows), the Verifier MUST extract the
|
|
164
|
+
# SignedCertificateTimestamp embedded in the leaf certificate, and verify it as in RFC 9162 §8.1.3,
|
|
165
|
+
# using the verification key from the Certificate Transparency Log.
|
|
166
|
+
chain = chains.first
|
|
167
|
+
if (result = verify_scts(bundle.leaf_certificate, chain)) && !result.verified?
|
|
168
|
+
return result
|
|
169
|
+
end
|
|
137
170
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
171
|
+
# 5)
|
|
172
|
+
# The Verifier MUST then check the certificate against the verification policy.
|
|
173
|
+
|
|
174
|
+
usage_ext = bundle.leaf_certificate.extension(Internal::X509::Extension::KeyUsage)
|
|
175
|
+
unless usage_ext.digital_signature
|
|
176
|
+
return VerificationFailure.new("Key usage is not of type `digital signature`")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
extended_key_usage = bundle.leaf_certificate.extension(Internal::X509::Extension::ExtendedKeyUsage)
|
|
180
|
+
unless extended_key_usage.code_signing?
|
|
181
|
+
return VerificationFailure.new("Extended key usage is not of type `code signing`")
|
|
182
|
+
end
|
|
142
183
|
|
|
143
|
-
|
|
144
|
-
|
|
184
|
+
policy_check = policy.verify(bundle.leaf_certificate)
|
|
185
|
+
return policy_check unless policy_check.verified?
|
|
186
|
+
end
|
|
145
187
|
|
|
146
188
|
# 6)
|
|
147
189
|
# By this point, the Verifier has already verified the signature by the Transparency Service (§Establishing a Time
|
|
@@ -156,10 +198,17 @@ module Sigstore
|
|
|
156
198
|
# * The key or certificate from the parsed body is the same as in the input certificate.
|
|
157
199
|
# * The “subject” of the parsed body matches the artifact.
|
|
158
200
|
|
|
159
|
-
signing_key = bundle.leaf_certificate.public_key
|
|
201
|
+
signing_key = bundle.key_based? ? key : bundle.leaf_certificate.public_key
|
|
160
202
|
|
|
161
203
|
case bundle.content
|
|
162
204
|
when :message_signature
|
|
205
|
+
# The messageDigest is an unauthenticated hint, but when present it must be
|
|
206
|
+
# consistent with the artifact being verified.
|
|
207
|
+
message_digest = bundle.message_signature.message_digest
|
|
208
|
+
if message_digest && !message_digest.digest.empty? && message_digest.digest != input.hashed_input.digest
|
|
209
|
+
return VerificationFailure.new("message digest does not match the artifact")
|
|
210
|
+
end
|
|
211
|
+
|
|
163
212
|
verified = verify_raw(signing_key, bundle.message_signature.signature, input.hashed_input.digest)
|
|
164
213
|
return VerificationFailure.new("Signature verification failed") unless verified
|
|
165
214
|
when :dsse_envelope
|
|
@@ -203,12 +252,9 @@ module Sigstore
|
|
|
203
252
|
end
|
|
204
253
|
|
|
205
254
|
def verify_dsse(dsse_envelope, public_key)
|
|
206
|
-
payload = dsse_envelope.payload
|
|
207
|
-
payload_type = dsse_envelope.payloadType
|
|
208
255
|
signatures = dsse_envelope.signatures
|
|
209
256
|
|
|
210
|
-
pae =
|
|
211
|
-
"#{payload.bytesize} #{payload}".b
|
|
257
|
+
pae = dsse_pae(dsse_envelope)
|
|
212
258
|
|
|
213
259
|
raise Error::InvalidBundle, "DSSEv1 envelope missing signatures" if signatures.empty?
|
|
214
260
|
|
|
@@ -217,6 +263,10 @@ module Sigstore
|
|
|
217
263
|
end
|
|
218
264
|
end
|
|
219
265
|
|
|
266
|
+
def dsse_pae(dsse_envelope)
|
|
267
|
+
Internal::Util.dsse_pae(dsse_envelope.payloadType, dsse_envelope.payload)
|
|
268
|
+
end
|
|
269
|
+
|
|
220
270
|
def verify_in_toto(input, in_toto_payload)
|
|
221
271
|
type = in_toto_payload["_type"]
|
|
222
272
|
raise Error::InvalidBundle, "Expected in-toto statement, got #{type.inspect}" unless type == "https://in-toto.io/Statement/v1"
|
|
@@ -349,7 +399,24 @@ module Sigstore
|
|
|
349
399
|
issuer
|
|
350
400
|
end
|
|
351
401
|
|
|
352
|
-
|
|
402
|
+
# The raw bytes a Timestamping Service response is expected to be computed over: the
|
|
403
|
+
# signature in the bundle (RFC 3161 message imprint covers these bytes).
|
|
404
|
+
def timestamped_data(bundle)
|
|
405
|
+
case bundle.content
|
|
406
|
+
when :message_signature
|
|
407
|
+
bundle.message_signature.signature
|
|
408
|
+
when :dsse_envelope
|
|
409
|
+
# The timestamp binds to a single signature; with more than one envelope
|
|
410
|
+
# signature it is ambiguous which one the imprint covers, so refuse to bind
|
|
411
|
+
# only the first (mirrors the v2 consistency path, which requires exactly one).
|
|
412
|
+
signatures = bundle.dsse_envelope.signatures
|
|
413
|
+
raise Error::InvalidBundle, "expected exactly one DSSE signature to bind a timestamp to" if signatures.size > 1
|
|
414
|
+
|
|
415
|
+
signatures.first&.sig
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def extract_timestamp_from_verification_data(data, signed_data)
|
|
353
420
|
# TODO: allow requiring a verified timestamp
|
|
354
421
|
unless data
|
|
355
422
|
logger.debug { "no timestamp verification data" }
|
|
@@ -365,6 +432,13 @@ module Sigstore
|
|
|
365
432
|
return
|
|
366
433
|
end
|
|
367
434
|
|
|
435
|
+
# The timestamp MUST be computed over the bundle signature (the RFC 3161 message
|
|
436
|
+
# imprint covers it). Without signature bytes to bind it to we cannot perform that
|
|
437
|
+
# binding, so fail closed rather than accept a timestamp over arbitrary data.
|
|
438
|
+
if signed_data.nil? || signed_data.empty?
|
|
439
|
+
raise Error::InvalidTimestamp, "no signature available to bind the timestamp to"
|
|
440
|
+
end
|
|
441
|
+
|
|
368
442
|
authorities = @timestamp_authorities.map do |ta|
|
|
369
443
|
store = OpenSSL::X509::Store.new
|
|
370
444
|
chain = ta.cert_chain.certificates.map do |cert|
|
|
@@ -381,19 +455,37 @@ module Sigstore
|
|
|
381
455
|
resp = OpenSSL::Timestamp::Response.new(ts.signed_timestamp)
|
|
382
456
|
|
|
383
457
|
req = OpenSSL::Timestamp::Request.new
|
|
384
|
-
req.cert_requested = !resp.token.certificates.empty?
|
|
385
|
-
# TODO: verify the message imprint against the signature in the bundle
|
|
458
|
+
req.cert_requested = !(resp.token.certificates.nil? || resp.token.certificates.empty?)
|
|
386
459
|
req.message_imprint = resp.token_info.message_imprint
|
|
387
460
|
req.algorithm = resp.token_info.algorithm
|
|
388
|
-
req.policy_id = resp.token_info.policy_id
|
|
389
|
-
req.nonce = resp.token_info.nonce
|
|
461
|
+
req.policy_id = resp.token_info.policy_id if resp.token_info.policy_id
|
|
462
|
+
req.nonce = resp.token_info.nonce if resp.token_info.nonce
|
|
390
463
|
req.version = resp.token_info.version
|
|
391
464
|
|
|
392
|
-
#
|
|
393
|
-
#
|
|
465
|
+
# The message imprint must cover the bundle's signature; otherwise the timestamp
|
|
466
|
+
# attests to unrelated data and must be rejected.
|
|
467
|
+
expected_imprint =
|
|
468
|
+
begin
|
|
469
|
+
OpenSSL::Digest.new(resp.token_info.algorithm).digest(signed_data)
|
|
470
|
+
rescue StandardError => e
|
|
471
|
+
raise Error::InvalidTimestamp,
|
|
472
|
+
"unsupported timestamp digest algorithm #{resp.token_info.algorithm.inspect}: #{e}"
|
|
473
|
+
end
|
|
474
|
+
unless resp.token_info.message_imprint == expected_imprint
|
|
475
|
+
raise Error::InvalidTimestamp, "timestamp message imprint does not match the bundle signature"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
verified = authorities.any? do |ta, chain, store|
|
|
479
|
+
# The timestamp must fall within the window the trusted root says this
|
|
480
|
+
# Timestamping Service was valid for, independent of cert-chain validity.
|
|
481
|
+
gen_time = resp.token_info.gen_time
|
|
482
|
+
valid_for = ta.valid_for
|
|
483
|
+
if valid_for
|
|
484
|
+
next false if valid_for.start && gen_time < valid_for.start.to_time
|
|
485
|
+
next false if valid_for.end && gen_time > valid_for.end.to_time
|
|
486
|
+
end
|
|
394
487
|
|
|
395
|
-
|
|
396
|
-
store.time = resp.token_info.gen_time
|
|
488
|
+
store.time = gen_time
|
|
397
489
|
|
|
398
490
|
resp.verify(req, store, chain) &&
|
|
399
491
|
(logger.debug do
|
|
@@ -402,13 +494,14 @@ module Sigstore
|
|
|
402
494
|
rescue OpenSSL::Timestamp::TimestampError => e
|
|
403
495
|
logger.error { "timestamp verification failed (#{e})" }
|
|
404
496
|
false
|
|
405
|
-
end
|
|
406
|
-
|
|
497
|
+
end
|
|
498
|
+
raise Error::InvalidTimestamp, "timestamp verification failed" unless verified
|
|
499
|
+
|
|
407
500
|
resp.token_info.gen_time
|
|
408
501
|
end
|
|
409
502
|
end
|
|
410
503
|
|
|
411
|
-
def find_rekor_entry(bundle, hashed_input, offline:)
|
|
504
|
+
def find_rekor_entry(bundle, hashed_input, offline:, signing_key: nil)
|
|
412
505
|
raise Error::InvalidBundle, "multiple tlog entries" if bundle.verification_material.tlog_entries.size > 1
|
|
413
506
|
|
|
414
507
|
rekor_entry = bundle.verification_material.tlog_entries&.first
|
|
@@ -420,7 +513,25 @@ module Sigstore
|
|
|
420
513
|
"has_inclusion_promise=#{has_inclusion_promise} has_inclusion_proof=#{has_inclusion_proof}"
|
|
421
514
|
end
|
|
422
515
|
|
|
423
|
-
|
|
516
|
+
# Rekor v2 (tiled) entries always ship an inclusion proof in the bundle and have no
|
|
517
|
+
# online retrieval API; detect them from the embedded entry and verify consistency
|
|
518
|
+
# against the bundle directly, rather than reconstructing a v1 canonicalized body.
|
|
519
|
+
if rekor_entry && (v2_body = rekor_v2_body(rekor_entry))
|
|
520
|
+
# A v2 entry has no integrated time and no online retrieval API, so the only
|
|
521
|
+
# binding to the log is the Merkle inclusion proof and its checkpoint signature.
|
|
522
|
+
# Require them here so the v2 path can never reach the offline warn-and-skip
|
|
523
|
+
# branch in #verify (even though #validate_version! also enforces this for
|
|
524
|
+
# non-0.1 bundles): without a checkpoint there is nothing to verify against.
|
|
525
|
+
unless rekor_entry.inclusion_proof&.checkpoint
|
|
526
|
+
raise Error::InvalidBundle, "Rekor v2 entry must contain an inclusion proof with a checkpoint"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
verify_rekor_v2_entry_consistency(bundle, hashed_input, v2_body, signing_key:)
|
|
530
|
+
return rekor_entry
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
verifier_pem = signing_key&.public_to_pem || bundle.leaf_certificate&.to_pem
|
|
534
|
+
expected_entry = bundle.expected_tlog_entry(hashed_input, verifier_pem)
|
|
424
535
|
|
|
425
536
|
entry = if offline
|
|
426
537
|
logger.debug { "Offline verification, skipping rekor" }
|
|
@@ -437,11 +548,7 @@ module Sigstore
|
|
|
437
548
|
|
|
438
549
|
logger.debug { "Found rekor entry: #{entry}" }
|
|
439
550
|
|
|
440
|
-
actual_body =
|
|
441
|
-
JSON.parse(entry.canonicalized_body)
|
|
442
|
-
rescue JSON::ParserError
|
|
443
|
-
raise Error::InvalidRekorEntry, "invalid JSON in rekor entry canonicalized_body"
|
|
444
|
-
end
|
|
551
|
+
actual_body = parse_canonicalized_body(entry)
|
|
445
552
|
if bundle.dsse_envelope?
|
|
446
553
|
# since the hash is over the uncanonicalized envelope, we need to remove it
|
|
447
554
|
#
|
|
@@ -471,6 +578,96 @@ module Sigstore
|
|
|
471
578
|
entry
|
|
472
579
|
end
|
|
473
580
|
|
|
581
|
+
# Parsed canonicalized body if this is a Rekor v2 (hashedrekord 0.0.2) entry, else nil.
|
|
582
|
+
# A body that is not JSON returns nil so the v1 reconstruction path handles it.
|
|
583
|
+
def rekor_v2_body(entry)
|
|
584
|
+
body = parse_canonicalized_body(entry)
|
|
585
|
+
body if body.values_at("kind", "apiVersion") == ["hashedrekord", "0.0.2"]
|
|
586
|
+
rescue Error::InvalidRekorEntry
|
|
587
|
+
nil
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def parse_canonicalized_body(entry)
|
|
591
|
+
JSON.parse(entry.canonicalized_body)
|
|
592
|
+
rescue JSON::ParserError
|
|
593
|
+
raise Error::InvalidRekorEntry, "invalid JSON in rekor entry canonicalized_body"
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Verify that a Rekor v2 hashedrekord (0.0.2) entry corresponds to the artifact,
|
|
597
|
+
# signature, and signing certificate in the bundle. The Merkle inclusion proof and
|
|
598
|
+
# checkpoint signature bind this body to the log; #find_rekor_entry has already
|
|
599
|
+
# confirmed both are present, and #verify performs that verification. Here we bind
|
|
600
|
+
# the body to the inputs we are verifying.
|
|
601
|
+
def verify_rekor_v2_entry_consistency(bundle, hashed_input, body, signing_key: nil)
|
|
602
|
+
spec = body.dig("spec", "hashedRekordV002")
|
|
603
|
+
raise Error::InvalidRekorEntry, "missing hashedRekordV002 spec" unless spec
|
|
604
|
+
|
|
605
|
+
logged_algorithm = spec.dig("data", "algorithm")
|
|
606
|
+
logged_digest = decode_base64_field(spec.dig("data", "digest"), "data.digest")
|
|
607
|
+
logged_signature = decode_base64_field(spec.dig("signature", "content"), "signature.content")
|
|
608
|
+
|
|
609
|
+
if bundle.key_based?
|
|
610
|
+
logged_key = decode_base64_field(
|
|
611
|
+
spec.dig("signature", "verifier", "publicKey", "rawBytes"),
|
|
612
|
+
"signature.verifier.publicKey.rawBytes"
|
|
613
|
+
)
|
|
614
|
+
unless logged_key == signing_key.public_to_der
|
|
615
|
+
raise Error::InvalidRekorEntry, "rekor entry public key does not match the supplied key"
|
|
616
|
+
end
|
|
617
|
+
else
|
|
618
|
+
logged_cert = decode_base64_field(
|
|
619
|
+
spec.dig("signature", "verifier", "x509Certificate", "rawBytes"),
|
|
620
|
+
"signature.verifier.x509Certificate.rawBytes"
|
|
621
|
+
)
|
|
622
|
+
unless logged_cert == bundle.leaf_certificate.to_der
|
|
623
|
+
raise Error::InvalidRekorEntry, "rekor entry certificate does not match the bundle certificate"
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
expected_algorithm, expected_digest, expected_signature =
|
|
628
|
+
rekor_v2_expected_data_and_signature(bundle, hashed_input)
|
|
629
|
+
|
|
630
|
+
unless logged_algorithm == expected_algorithm
|
|
631
|
+
raise Error::InvalidRekorEntry,
|
|
632
|
+
"rekor entry data algorithm #{logged_algorithm.inspect} does not match " \
|
|
633
|
+
"expected #{expected_algorithm.inspect}"
|
|
634
|
+
end
|
|
635
|
+
unless logged_digest == expected_digest
|
|
636
|
+
raise Error::InvalidRekorEntry, "rekor entry data digest does not match the artifact"
|
|
637
|
+
end
|
|
638
|
+
return if logged_signature == expected_signature
|
|
639
|
+
|
|
640
|
+
raise Error::InvalidRekorEntry, "rekor entry signature does not match the bundle signature"
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# The expected [data.algorithm, data.digest, signature] for the bundle, named to match
|
|
644
|
+
# the Rekor v2 `hashedRekordV002` spec fields (algorithm uses the proto enum name).
|
|
645
|
+
def rekor_v2_expected_data_and_signature(bundle, hashed_input)
|
|
646
|
+
case bundle.content
|
|
647
|
+
when :message_signature
|
|
648
|
+
[hashed_input.algorithm.name, hashed_input.digest, bundle.message_signature.signature]
|
|
649
|
+
when :dsse_envelope
|
|
650
|
+
# "hashedrekord-over-DSSE": the logged data is SHA2-256 of PAE(payloadType, payload)
|
|
651
|
+
# and the logged signature is the (single) DSSE envelope signature.
|
|
652
|
+
signatures = bundle.dsse_envelope.signatures
|
|
653
|
+
unless signatures.size == 1
|
|
654
|
+
raise Error::InvalidRekorEntry, "expected exactly one DSSE signature for a Rekor v2 entry"
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
["SHA2_256", OpenSSL::Digest::SHA256.digest(dsse_pae(bundle.dsse_envelope)), signatures.first.sig]
|
|
658
|
+
else
|
|
659
|
+
raise Error::InvalidBundle, "expected either message_signature or dsse_envelope"
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def decode_base64_field(value, name)
|
|
664
|
+
raise Error::InvalidRekorEntry, "missing #{name} in rekor entry" unless value
|
|
665
|
+
|
|
666
|
+
Internal::Util.base64_decode(value)
|
|
667
|
+
rescue ArgumentError
|
|
668
|
+
raise Error::InvalidRekorEntry, "invalid base64 in #{name} of rekor entry"
|
|
669
|
+
end
|
|
670
|
+
|
|
474
671
|
def diff_json(a, b) # rubocop:disable Naming/MethodParameterName
|
|
475
672
|
return nil if a == b
|
|
476
673
|
|
data/lib/sigstore/version.rb
CHANGED
data/lib/sigstore.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sigstore
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Sigstore Authors
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-11-02 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: logger
|
|
@@ -39,20 +39,34 @@ dependencies:
|
|
|
39
39
|
- - ">="
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
41
|
version: '0'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: protobug_in_toto_attestation_protos
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - "~>"
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 0.2.0
|
|
49
|
+
type: :runtime
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - "~>"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 0.2.0
|
|
42
56
|
- !ruby/object:Gem::Dependency
|
|
43
57
|
name: protobug_sigstore_protos
|
|
44
58
|
requirement: !ruby/object:Gem::Requirement
|
|
45
59
|
requirements:
|
|
46
60
|
- - "~>"
|
|
47
61
|
- !ruby/object:Gem::Version
|
|
48
|
-
version: 0.
|
|
62
|
+
version: 0.2.0
|
|
49
63
|
type: :runtime
|
|
50
64
|
prerelease: false
|
|
51
65
|
version_requirements: !ruby/object:Gem::Requirement
|
|
52
66
|
requirements:
|
|
53
67
|
- - "~>"
|
|
54
68
|
- !ruby/object:Gem::Version
|
|
55
|
-
version: 0.
|
|
69
|
+
version: 0.2.0
|
|
56
70
|
- !ruby/object:Gem::Dependency
|
|
57
71
|
name: uri
|
|
58
72
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -98,6 +112,7 @@ files:
|
|
|
98
112
|
- lib/sigstore/rekor/checkpoint.rb
|
|
99
113
|
- lib/sigstore/rekor/client.rb
|
|
100
114
|
- lib/sigstore/signer.rb
|
|
115
|
+
- lib/sigstore/signing_config.rb
|
|
101
116
|
- lib/sigstore/trusted_root.rb
|
|
102
117
|
- lib/sigstore/tuf.rb
|
|
103
118
|
- lib/sigstore/tuf/config.rb
|