sigstore 0.2.2 → 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/merkle.rb +2 -2
- data/lib/sigstore/internal/util.rb +7 -0
- data/lib/sigstore/internal/x509.rb +7 -7
- data/lib/sigstore/models.rb +56 -16
- data/lib/sigstore/oidc.rb +3 -0
- data/lib/sigstore/policy.rb +8 -0
- data/lib/sigstore/rekor/checkpoint.rb +21 -11
- data/lib/sigstore/rekor/client.rb +38 -2
- data/lib/sigstore/signer.rb +248 -36
- data/lib/sigstore/signing_config.rb +181 -0
- data/lib/sigstore/trusted_root.rb +20 -4
- data/lib/sigstore/tuf.rb +16 -2
- data/lib/sigstore/verifier.rb +283 -73
- data/lib/sigstore/version.rb +2 -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
|
-
return VerificationFailure.new("Extended key usage is not of type `code signing`")
|
|
141
|
-
end
|
|
171
|
+
# 5)
|
|
172
|
+
# The Verifier MUST then check the certificate against the verification policy.
|
|
142
173
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
183
|
+
|
|
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
|
|
@@ -173,7 +222,9 @@ module Sigstore
|
|
|
173
222
|
rescue JSON::ParserError
|
|
174
223
|
raise Error::InvalidBundle, "invalid JSON for in-toto statement in DSSE payload"
|
|
175
224
|
end
|
|
176
|
-
verify_in_toto(input, in_toto)
|
|
225
|
+
if (result = verify_in_toto(input, in_toto))
|
|
226
|
+
return result
|
|
227
|
+
end
|
|
177
228
|
else
|
|
178
229
|
raise Sigstore::Error::Unimplemented,
|
|
179
230
|
"unsupported DSSE payload type: #{bundle.dsse_envelope.payloadType.inspect}"
|
|
@@ -201,12 +252,9 @@ module Sigstore
|
|
|
201
252
|
end
|
|
202
253
|
|
|
203
254
|
def verify_dsse(dsse_envelope, public_key)
|
|
204
|
-
payload = dsse_envelope.payload
|
|
205
|
-
payload_type = dsse_envelope.payloadType
|
|
206
255
|
signatures = dsse_envelope.signatures
|
|
207
256
|
|
|
208
|
-
pae =
|
|
209
|
-
"#{payload.bytesize} #{payload}".b
|
|
257
|
+
pae = dsse_pae(dsse_envelope)
|
|
210
258
|
|
|
211
259
|
raise Error::InvalidBundle, "DSSEv1 envelope missing signatures" if signatures.empty?
|
|
212
260
|
|
|
@@ -215,26 +263,32 @@ module Sigstore
|
|
|
215
263
|
end
|
|
216
264
|
end
|
|
217
265
|
|
|
266
|
+
def dsse_pae(dsse_envelope)
|
|
267
|
+
Internal::Util.dsse_pae(dsse_envelope.payloadType, dsse_envelope.payload)
|
|
268
|
+
end
|
|
269
|
+
|
|
218
270
|
def verify_in_toto(input, in_toto_payload)
|
|
219
|
-
type = in_toto_payload
|
|
271
|
+
type = in_toto_payload["_type"]
|
|
220
272
|
raise Error::InvalidBundle, "Expected in-toto statement, got #{type.inspect}" unless type == "https://in-toto.io/Statement/v1"
|
|
221
273
|
|
|
222
|
-
|
|
223
|
-
raise Error::InvalidBundle, "Expected in-toto statement with subject"
|
|
224
|
-
|
|
225
|
-
subject = subject.first
|
|
226
|
-
digest = subject.fetch("digest")
|
|
227
|
-
raise Error::InvalidBundle, "Expected in-toto statement with digest" if !digest || digest.empty?
|
|
274
|
+
subjects = in_toto_payload["subject"]
|
|
275
|
+
raise Error::InvalidBundle, "Expected in-toto statement with subject" if !subjects || subjects.empty?
|
|
228
276
|
|
|
277
|
+
expected_algorithm = Internal::Util.hash_algorithm_name(input.hashed_input.algorithm)
|
|
229
278
|
expected_hexdigest = Internal::Util.hex_encode(input.hashed_input.digest)
|
|
230
|
-
digest.each do |name, value|
|
|
231
|
-
next if expected_hexdigest == value
|
|
232
279
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
280
|
+
matched = subjects.map do |subject|
|
|
281
|
+
digest = subject["digest"]
|
|
282
|
+
raise Error::InvalidBundle, "Expected in-toto statement with digest" if !digest || digest.empty?
|
|
283
|
+
|
|
284
|
+
digest[expected_algorithm] == expected_hexdigest
|
|
285
|
+
end.any?
|
|
286
|
+
|
|
287
|
+
return if matched
|
|
288
|
+
|
|
289
|
+
VerificationFailure.new(
|
|
290
|
+
"None of in-toto subjects matches artifact for #{expected_algorithm}: #{expected_hexdigest}"
|
|
291
|
+
)
|
|
238
292
|
end
|
|
239
293
|
|
|
240
294
|
public
|
|
@@ -318,13 +372,22 @@ module Sigstore
|
|
|
318
372
|
raise Error::Unimplemented, "only x509_entry and precert_entry supported, given #{sct.entry_type.inspect}"
|
|
319
373
|
end
|
|
320
374
|
|
|
321
|
-
[
|
|
375
|
+
[
|
|
376
|
+
sct.version,
|
|
377
|
+
0,
|
|
378
|
+
sct.timestamp,
|
|
379
|
+
sct.entry_type,
|
|
380
|
+
signed_entry,
|
|
381
|
+
sct.extensions_bytes&.bytesize.to_i,
|
|
382
|
+
sct.extensions_bytes
|
|
383
|
+
].pack(<<~PACK)
|
|
322
384
|
C # version
|
|
323
385
|
C # signature_type
|
|
324
386
|
Q> # timestamp
|
|
325
387
|
n # entry_type
|
|
326
388
|
a#{signed_entry.bytesize} # signed_entry
|
|
327
389
|
n # extensions length
|
|
390
|
+
a#{sct.extensions_bytes&.bytesize.to_i} # extension
|
|
328
391
|
PACK
|
|
329
392
|
end
|
|
330
393
|
|
|
@@ -336,7 +399,24 @@ module Sigstore
|
|
|
336
399
|
issuer
|
|
337
400
|
end
|
|
338
401
|
|
|
339
|
-
|
|
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)
|
|
340
420
|
# TODO: allow requiring a verified timestamp
|
|
341
421
|
unless data
|
|
342
422
|
logger.debug { "no timestamp verification data" }
|
|
@@ -352,6 +432,13 @@ module Sigstore
|
|
|
352
432
|
return
|
|
353
433
|
end
|
|
354
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
|
+
|
|
355
442
|
authorities = @timestamp_authorities.map do |ta|
|
|
356
443
|
store = OpenSSL::X509::Store.new
|
|
357
444
|
chain = ta.cert_chain.certificates.map do |cert|
|
|
@@ -368,19 +455,37 @@ module Sigstore
|
|
|
368
455
|
resp = OpenSSL::Timestamp::Response.new(ts.signed_timestamp)
|
|
369
456
|
|
|
370
457
|
req = OpenSSL::Timestamp::Request.new
|
|
371
|
-
req.cert_requested = !resp.token.certificates.empty?
|
|
372
|
-
# TODO: verify the message imprint against the signature in the bundle
|
|
458
|
+
req.cert_requested = !(resp.token.certificates.nil? || resp.token.certificates.empty?)
|
|
373
459
|
req.message_imprint = resp.token_info.message_imprint
|
|
374
460
|
req.algorithm = resp.token_info.algorithm
|
|
375
|
-
req.policy_id = resp.token_info.policy_id
|
|
376
|
-
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
|
|
377
463
|
req.version = resp.token_info.version
|
|
378
464
|
|
|
379
|
-
#
|
|
380
|
-
#
|
|
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
|
|
381
487
|
|
|
382
|
-
|
|
383
|
-
store.time = resp.token_info.gen_time
|
|
488
|
+
store.time = gen_time
|
|
384
489
|
|
|
385
490
|
resp.verify(req, store, chain) &&
|
|
386
491
|
(logger.debug do
|
|
@@ -389,13 +494,14 @@ module Sigstore
|
|
|
389
494
|
rescue OpenSSL::Timestamp::TimestampError => e
|
|
390
495
|
logger.error { "timestamp verification failed (#{e})" }
|
|
391
496
|
false
|
|
392
|
-
end
|
|
393
|
-
|
|
497
|
+
end
|
|
498
|
+
raise Error::InvalidTimestamp, "timestamp verification failed" unless verified
|
|
499
|
+
|
|
394
500
|
resp.token_info.gen_time
|
|
395
501
|
end
|
|
396
502
|
end
|
|
397
503
|
|
|
398
|
-
def find_rekor_entry(bundle, hashed_input, offline:)
|
|
504
|
+
def find_rekor_entry(bundle, hashed_input, offline:, signing_key: nil)
|
|
399
505
|
raise Error::InvalidBundle, "multiple tlog entries" if bundle.verification_material.tlog_entries.size > 1
|
|
400
506
|
|
|
401
507
|
rekor_entry = bundle.verification_material.tlog_entries&.first
|
|
@@ -407,7 +513,25 @@ module Sigstore
|
|
|
407
513
|
"has_inclusion_promise=#{has_inclusion_promise} has_inclusion_proof=#{has_inclusion_proof}"
|
|
408
514
|
end
|
|
409
515
|
|
|
410
|
-
|
|
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)
|
|
411
535
|
|
|
412
536
|
entry = if offline
|
|
413
537
|
logger.debug { "Offline verification, skipping rekor" }
|
|
@@ -424,11 +548,7 @@ module Sigstore
|
|
|
424
548
|
|
|
425
549
|
logger.debug { "Found rekor entry: #{entry}" }
|
|
426
550
|
|
|
427
|
-
actual_body =
|
|
428
|
-
JSON.parse(entry.canonicalized_body)
|
|
429
|
-
rescue JSON::ParserError
|
|
430
|
-
raise Error::InvalidRekorEntry, "invalid JSON in rekor entry canonicalized_body"
|
|
431
|
-
end
|
|
551
|
+
actual_body = parse_canonicalized_body(entry)
|
|
432
552
|
if bundle.dsse_envelope?
|
|
433
553
|
# since the hash is over the uncanonicalized envelope, we need to remove it
|
|
434
554
|
#
|
|
@@ -458,6 +578,96 @@ module Sigstore
|
|
|
458
578
|
entry
|
|
459
579
|
end
|
|
460
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
|
+
|
|
461
671
|
def diff_json(a, b) # rubocop:disable Naming/MethodParameterName
|
|
462
672
|
return nil if a == b
|
|
463
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:
|
|
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
|