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.
@@ -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
- def verify(input:, policy:, offline:)
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
- timestamps = extract_timestamp_from_verification_data(materials.timestamp_verification_data) || []
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
- timestamps << Time.at(entry.integrated_time).utc
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
- chains = timestamps.map do |ts|
109
- chain, err = Internal::X509.validate_chain(@fulcio_cert_chains, bundle.leaf_certificate, ts)
110
- return err if err
111
-
112
- chain
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
- chains.uniq! { |chain| chain.map(&:to_der) }
116
- unless chains.size == 1
117
- raise "expected exactly one certificate chain, got #{chains.size} chains:\n" +
118
- chains.map do |chain|
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
- # 4)
124
- # Unless performing online verification (see §Alternative Workflows), the Verifier MUST extract the
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
- # 5)
133
- # The Verifier MUST then check the certificate against the verification policy.
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
- usage_ext = bundle.leaf_certificate.extension(Internal::X509::Extension::KeyUsage)
136
- return VerificationFailure.new("Key usage is not of type `digital signature`") unless usage_ext.digital_signature
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
- extended_key_usage = bundle.leaf_certificate.extension(Internal::X509::Extension::ExtendedKeyUsage)
139
- unless extended_key_usage.code_signing?
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
- policy_check = policy.verify(bundle.leaf_certificate)
144
- return policy_check unless policy_check.verified?
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 = "DSSEv1 #{payload_type.bytesize} #{payload_type} " \
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.fetch("_type")
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
- subject = in_toto_payload.fetch("subject")
223
- raise Error::InvalidBundle, "Expected in-toto statement with subject" unless subject && subject.size == 1
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
- return VerificationFailure.new(
234
- "in-toto subject does not match for #{input.hashed_input.algorithm} of #{subject.fetch("name")}: " \
235
- "expected #{name} to be #{value}, got #{expected_hexdigest}"
236
- )
237
- end
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
- [sct.version, 0, sct.timestamp, sct.entry_type, signed_entry, 0].pack(<<~PACK)
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
- def extract_timestamp_from_verification_data(data)
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
- # TODO: verify the hashed message in the message imprint
380
- # against the signature in the bundle
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
- authorities.any? do |ta, chain, store|
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
- raise(OpenSSL::Timestamp::TimestampError, "timestamp verification failed")
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
- expected_entry = bundle.expected_tlog_entry(hashed_input)
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 = begin
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
 
@@ -15,5 +15,6 @@
15
15
  # limitations under the License.
16
16
 
17
17
  module Sigstore
18
- VERSION = "0.2.2"
18
+ VERSION = "0.3.0"
19
+ USER_AGENT = "sigstore-ruby/#{VERSION}".freeze
19
20
  end
data/lib/sigstore.rb CHANGED
@@ -43,5 +43,6 @@ module Sigstore
43
43
  end
44
44
  end
45
45
 
46
+ require_relative "sigstore/signing_config"
46
47
  require_relative "sigstore/verifier"
47
48
  require_relative "sigstore/signer"
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.2.2
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: 2025-10-25 00:00:00.000000000 Z
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.1.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.1.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