sigstore 0.1.1
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 +7 -0
- data/CHANGELOG.md +7 -0
- data/CODEOWNERS +6 -0
- data/LICENSE +201 -0
- data/README.md +26 -0
- data/data/_store/prod/root.json +165 -0
- data/data/_store/prod/trusted_root.json +114 -0
- data/data/_store/staging/root.json +107 -0
- data/data/_store/staging/trusted_root.json +87 -0
- data/lib/sigstore/error.rb +43 -0
- data/lib/sigstore/internal/json.rb +53 -0
- data/lib/sigstore/internal/key.rb +183 -0
- data/lib/sigstore/internal/keyring.rb +42 -0
- data/lib/sigstore/internal/merkle.rb +117 -0
- data/lib/sigstore/internal/set.rb +42 -0
- data/lib/sigstore/internal/util.rb +52 -0
- data/lib/sigstore/internal/x509.rb +460 -0
- data/lib/sigstore/models.rb +272 -0
- data/lib/sigstore/oidc.rb +149 -0
- data/lib/sigstore/policy.rb +104 -0
- data/lib/sigstore/rekor/checkpoint.rb +114 -0
- data/lib/sigstore/rekor/client.rb +136 -0
- data/lib/sigstore/signer.rb +280 -0
- data/lib/sigstore/trusted_root.rb +116 -0
- data/lib/sigstore/tuf/config.rb +46 -0
- data/lib/sigstore/tuf/error.rb +49 -0
- data/lib/sigstore/tuf/file.rb +96 -0
- data/lib/sigstore/tuf/keys.rb +42 -0
- data/lib/sigstore/tuf/roles.rb +106 -0
- data/lib/sigstore/tuf/root.rb +53 -0
- data/lib/sigstore/tuf/snapshot.rb +45 -0
- data/lib/sigstore/tuf/targets.rb +84 -0
- data/lib/sigstore/tuf/timestamp.rb +39 -0
- data/lib/sigstore/tuf/trusted_metadata_set.rb +193 -0
- data/lib/sigstore/tuf/updater.rb +267 -0
- data/lib/sigstore/tuf.rb +158 -0
- data/lib/sigstore/verifier.rb +492 -0
- data/lib/sigstore/version.rb +19 -0
- data/lib/sigstore.rb +44 -0
- metadata +128 -0
@@ -0,0 +1,492 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2024 The Sigstore Authors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require_relative "trusted_root"
|
18
|
+
require_relative "internal/keyring"
|
19
|
+
require_relative "internal/merkle"
|
20
|
+
require_relative "internal/set"
|
21
|
+
require_relative "rekor/client"
|
22
|
+
require_relative "rekor/checkpoint"
|
23
|
+
require_relative "internal/x509"
|
24
|
+
|
25
|
+
module Sigstore
|
26
|
+
class Verifier
|
27
|
+
include Loggable
|
28
|
+
|
29
|
+
attr_reader :rekor_client
|
30
|
+
|
31
|
+
def initialize(rekor_client:, fulcio_cert_chain:, timestamp_authorities:, rekor_keyring:, ct_keyring:)
|
32
|
+
@rekor_client = rekor_client
|
33
|
+
@fulcio_cert_chain = fulcio_cert_chain
|
34
|
+
@timestamp_authorities = timestamp_authorities
|
35
|
+
@rekor_keyring = rekor_keyring
|
36
|
+
@ct_keyring = ct_keyring
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.for_trust_root(trust_root:)
|
40
|
+
new(
|
41
|
+
rekor_client: Rekor::Client.new(url: trust_root.tlog_for_signing.base_url),
|
42
|
+
fulcio_cert_chain: trust_root.fulcio_cert_chain,
|
43
|
+
timestamp_authorities: trust_root.timestamp_authorities,
|
44
|
+
rekor_keyring: Internal::Keyring.new(keys: trust_root.rekor_keys),
|
45
|
+
ct_keyring: Internal::Keyring.new(keys: trust_root.ctfe_keys)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.production(trust_root: TrustedRoot.production)
|
50
|
+
for_trust_root(trust_root:)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.staging(trust_root: TrustedRoot.staging)
|
54
|
+
for_trust_root(trust_root:)
|
55
|
+
end
|
56
|
+
|
57
|
+
def verify(input:, policy:, offline:)
|
58
|
+
# First, establish a time for the signature. This timestamp is required to validate the certificate chain,
|
59
|
+
# so this step comes first.
|
60
|
+
|
61
|
+
bundle = input.sbundle
|
62
|
+
materials = bundle.verification_material
|
63
|
+
|
64
|
+
# 1)
|
65
|
+
# If the verification policy uses the Timestamping Service, the Verifier MUST verify the timestamping response
|
66
|
+
# using the Timestamping Service root key material, as described in Spec: Timestamping Service, with the raw bytes
|
67
|
+
# of the signature as the timestamped data. The Verifier MUST then extract a timestamp from the timestamping
|
68
|
+
# response. If verification or timestamp parsing fails, the Verifier MUST abort.
|
69
|
+
|
70
|
+
timestamps = extract_timestamp_from_verification_data(materials.timestamp_verification_data) || []
|
71
|
+
|
72
|
+
# 2)
|
73
|
+
# If the verification policy uses timestamps from the Transparency Service, the Verifier MUST verify the signature
|
74
|
+
# on the Transparency Service LogEntry as described in Spec: Transparency Service against the pre-distributed root
|
75
|
+
# key material from the transparency service. The Verifier SHOULD NOT (yet) attempt to parse the body.
|
76
|
+
# The Verifier MUST then parse the integratedTime as a Unix timestamp (seconds since January 1, 1970 UTC).
|
77
|
+
# If verification or timestamp parsing fails, the Verifier MUST abort.
|
78
|
+
|
79
|
+
begin
|
80
|
+
# TODO: should this instead be an input to the verify method?
|
81
|
+
# See https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit?disco=AAABQVV-gT0
|
82
|
+
entry = find_rekor_entry(bundle, input.hashed_input, offline:)
|
83
|
+
rescue Sigstore::Error::MissingRekorEntry
|
84
|
+
return VerificationFailure.new("Rekor entry not found")
|
85
|
+
else
|
86
|
+
if entry.inclusion_proof&.checkpoint
|
87
|
+
Internal::Merkle.verify_merkle_inclusion(entry)
|
88
|
+
Rekor::Checkpoint.verify_checkpoint(@rekor_keyring, entry)
|
89
|
+
elsif !offline
|
90
|
+
return VerificationFailure.new("Missing Rekor inclusion proof")
|
91
|
+
else
|
92
|
+
logger.warn "inclusion proof not present in bundle: skipping due to offline verification"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
Internal::SET.verify_set(keyring: @rekor_keyring, entry:) if entry.inclusion_promise
|
97
|
+
|
98
|
+
timestamps << Time.at(entry.integrated_time).utc
|
99
|
+
|
100
|
+
# TODO: implement this step
|
101
|
+
|
102
|
+
store = OpenSSL::X509::Store.new
|
103
|
+
|
104
|
+
@fulcio_cert_chain.each do |cert|
|
105
|
+
store.add_cert(cert.openssl)
|
106
|
+
end
|
107
|
+
|
108
|
+
# 3)
|
109
|
+
# The Verifier MUST perform certification path validation (RFC 5280 §6) of the certificate chain with the
|
110
|
+
# pre-distributed Fulcio root certificate(s) as a trust anchor, but with a fake “current time.”
|
111
|
+
# If a timestamp from the timestamping service is available, the Verifier MUST perform path validation using the
|
112
|
+
# timestamp from the Timestamping Service. If a timestamp from the Transparency Service is available, the Verifier
|
113
|
+
# MUST perform path validation using the timestamp from the Transparency Service. If both are available, the
|
114
|
+
# Verifier performs path validation twice. If either fails, verification fails.
|
115
|
+
chains = timestamps.map do |ts|
|
116
|
+
store_ctx = OpenSSL::X509::StoreContext.new(store, bundle.leaf_certificate.openssl)
|
117
|
+
store_ctx.time = ts
|
118
|
+
|
119
|
+
unless store_ctx.verify
|
120
|
+
return VerificationFailure.new(
|
121
|
+
"failed to validate certification from fulcio cert chain: #{store_ctx.error_string}"
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
chain = store_ctx.chain || raise(Error::InvalidCertificate, "no valid cert chain found")
|
126
|
+
chain.shift # remove the cert itself
|
127
|
+
chain.map! { Internal::X509::Certificate.new(_1) }
|
128
|
+
end
|
129
|
+
|
130
|
+
chains.uniq! { |chain| chain.map(&:to_der) }
|
131
|
+
unless chains.size == 1
|
132
|
+
raise "expected exactly one certificate chain, got #{chains.size} chains:\n" +
|
133
|
+
chains.map do |chain|
|
134
|
+
chain.map(&:to_text).join("\n")
|
135
|
+
end.join("\n\n")
|
136
|
+
end
|
137
|
+
|
138
|
+
# 4)
|
139
|
+
# Unless performing online verification (see §Alternative Workflows), the Verifier MUST extract the
|
140
|
+
# SignedCertificateTimestamp embedded in the leaf certificate, and verify it as in RFC 9162 §8.1.3,
|
141
|
+
# using the verification key from the Certificate Transparency Log.
|
142
|
+
chain = chains.first
|
143
|
+
if (result = verify_scts(bundle.leaf_certificate, chain)) && !result.verified?
|
144
|
+
return result
|
145
|
+
end
|
146
|
+
|
147
|
+
# 5)
|
148
|
+
# The Verifier MUST then check the certificate against the verification policy.
|
149
|
+
|
150
|
+
usage_ext = bundle.leaf_certificate.extension(Internal::X509::Extension::KeyUsage)
|
151
|
+
return VerificationFailure.new("Key usage is not of type `digital signature`") unless usage_ext.digital_signature
|
152
|
+
|
153
|
+
extended_key_usage = bundle.leaf_certificate.extension(Internal::X509::Extension::ExtendedKeyUsage)
|
154
|
+
unless extended_key_usage.code_signing?
|
155
|
+
return VerificationFailure.new("Extended key usage is not of type `code signing`")
|
156
|
+
end
|
157
|
+
|
158
|
+
policy_check = policy.verify(bundle.leaf_certificate)
|
159
|
+
return policy_check unless policy_check.verified?
|
160
|
+
|
161
|
+
# 6)
|
162
|
+
# By this point, the Verifier has already verified the signature by the Transparency Service (§Establishing a Time
|
163
|
+
# for the Signature). The Verifier MUST parse body: body is a base64-encoded JSON document with keys apiVersion
|
164
|
+
# and kind. The Verifier implementation contains a list of known Transparency Service formats (by apiVersion and
|
165
|
+
# kind); if no type is found, abort. The Verifier MUST parse body as the given type.
|
166
|
+
#
|
167
|
+
# Then, the Verifier MUST check the following; exactly how to do this will be specified by each type in Spec:
|
168
|
+
# Sigstore Registries (§Signature Metadata Formats):
|
169
|
+
#
|
170
|
+
# * The signature from the parsed body is the same as the provided signature.
|
171
|
+
# * The key or certificate from the parsed body is the same as in the input certificate.
|
172
|
+
# * The “subject” of the parsed body matches the artifact.
|
173
|
+
|
174
|
+
signing_key = bundle.leaf_certificate.public_key
|
175
|
+
|
176
|
+
case bundle.content
|
177
|
+
when :message_signature
|
178
|
+
verified = verify_raw(signing_key, bundle.message_signature.signature, input.hashed_input.digest)
|
179
|
+
return VerificationFailure.new("Signature verification failed") unless verified
|
180
|
+
when :dsse_envelope
|
181
|
+
verify_dsse(bundle.dsse_envelope, signing_key) or
|
182
|
+
return VerificationFailure.new("DSSE envelope verification failed")
|
183
|
+
|
184
|
+
case bundle.dsse_envelope.payloadType
|
185
|
+
when "application/vnd.in-toto+json"
|
186
|
+
verify_in_toto(input, JSON.parse(bundle.dsse_envelope.payload))
|
187
|
+
else
|
188
|
+
raise Sigstore::Error::Unimplemented,
|
189
|
+
"unsupported DSSE payload type: #{bundle.dsse_envelope.payloadType.inspect}"
|
190
|
+
end
|
191
|
+
else
|
192
|
+
raise Error::InvalidBundle, "unknown content type: #{bundle.content}"
|
193
|
+
end
|
194
|
+
|
195
|
+
VerificationSuccess.new
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def verify_raw(public_key, signature, data)
|
201
|
+
if public_key.respond_to?(:verify_raw)
|
202
|
+
public_key.verify_raw(nil, signature, data)
|
203
|
+
else
|
204
|
+
case public_key
|
205
|
+
when OpenSSL::PKey::EC
|
206
|
+
public_key.dsa_verify_asn1(data, signature)
|
207
|
+
else
|
208
|
+
raise Error::Unimplemented, "unsupported public key type: #{public_key.class} for raw verification"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def verify_dsse(dsse_envelope, public_key)
|
214
|
+
payload = dsse_envelope.payload
|
215
|
+
payload_type = dsse_envelope.payloadType
|
216
|
+
signatures = dsse_envelope.signatures
|
217
|
+
|
218
|
+
pae = "DSSEv1 #{payload_type.bytesize} #{payload_type} " \
|
219
|
+
"#{payload.bytesize} #{payload}".b
|
220
|
+
|
221
|
+
raise Error::InvalidBundle, "DSSEv1 envelope missing signatures" if signatures.empty?
|
222
|
+
|
223
|
+
signatures.all? do |signature|
|
224
|
+
public_key.verify("SHA256", signature.sig, pae)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def verify_in_toto(input, in_toto_payload)
|
229
|
+
type = in_toto_payload.fetch("_type")
|
230
|
+
raise Error::InvalidBundle, "Expected in-toto statement, got #{type.inspect}" unless type == "https://in-toto.io/Statement/v1"
|
231
|
+
|
232
|
+
subject = in_toto_payload.fetch("subject")
|
233
|
+
raise Error::InvalidBundle, "Expected in-toto statement with subject" unless subject && subject.size == 1
|
234
|
+
|
235
|
+
subject = subject.first
|
236
|
+
digest = subject.fetch("digest")
|
237
|
+
raise Error::InvalidBundle, "Expected in-toto statement with digest" if !digest || digest.empty?
|
238
|
+
|
239
|
+
expected_hexdigest = Internal::Util.hex_encode(input.hashed_input.digest)
|
240
|
+
digest.each do |name, value|
|
241
|
+
next if expected_hexdigest == value
|
242
|
+
|
243
|
+
return VerificationFailure.new(
|
244
|
+
"in-toto subject does not match for #{input.hashed_input.algorithm} of #{subject.fetch("name")}: " \
|
245
|
+
"expected #{name} to be #{value}, got #{expected_hexdigest}"
|
246
|
+
)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
public
|
251
|
+
|
252
|
+
def verify_scts(leaf_certificate, chain)
|
253
|
+
sct_list = leaf_certificate
|
254
|
+
.extension(Internal::X509::Extension::PrecertificateSignedCertificateTimestamps)
|
255
|
+
.signed_certificate_timestamps
|
256
|
+
raise Error::InvalidCertificate, "no SCTs found" if sct_list.empty?
|
257
|
+
|
258
|
+
sct_list.each do |sct|
|
259
|
+
verified = verify_sct(
|
260
|
+
sct,
|
261
|
+
leaf_certificate,
|
262
|
+
chain,
|
263
|
+
@ct_keyring
|
264
|
+
)
|
265
|
+
return VerificationFailure.new("SCT verification failed") unless verified
|
266
|
+
end
|
267
|
+
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
|
271
|
+
private
|
272
|
+
|
273
|
+
def verify_sct(sct, certificate, chain, ct_keyring)
|
274
|
+
if sct.entry_type == 1
|
275
|
+
issuer_cert = find_issuer_cert(chain)
|
276
|
+
issuer_pubkey = issuer_cert.public_key
|
277
|
+
unless issuer_cert.ca?
|
278
|
+
raise Error::InvalidCertificate, "Invalid issuer pubkey basicConstraint (not a CA): #{issuer_cert.to_text}"
|
279
|
+
end
|
280
|
+
|
281
|
+
issuer_key_id = OpenSSL::Digest::SHA256.digest(issuer_pubkey.public_to_der)
|
282
|
+
end
|
283
|
+
|
284
|
+
digitally_signed = pack_digitally_signed(sct, certificate, issuer_key_id).b
|
285
|
+
ct_keyring.verify(key_id: sct.log_id, signature: sct.signature, data: digitally_signed)
|
286
|
+
end
|
287
|
+
|
288
|
+
def pack_digitally_signed(sct, certificate, issuer_key_id = nil)
|
289
|
+
# https://datatracker.ietf.org/doc/html/rfc6962#section-3.4
|
290
|
+
# https://datatracker.ietf.org/doc/html/rfc6962#section-3.5
|
291
|
+
#
|
292
|
+
# digitally-signed struct {
|
293
|
+
# Version sct_version;
|
294
|
+
# SignatureType signature_type = certificate_timestamp;
|
295
|
+
# uint64 timestamp;
|
296
|
+
# LogEntryType entry_type;
|
297
|
+
# select(entry_type) {
|
298
|
+
# case x509_entry: ASN.1Cert;
|
299
|
+
# case precert_entry: PreCert;
|
300
|
+
# } signed_entry;
|
301
|
+
# CtExtensions extensions;
|
302
|
+
# };
|
303
|
+
|
304
|
+
signed_entry =
|
305
|
+
case sct.entry_type
|
306
|
+
when 0 # x509_entry
|
307
|
+
cert_der = certificate.to_public_der
|
308
|
+
cert_len = cert_der.bytesize
|
309
|
+
unused, len1, len2, len3 = [cert_len].pack("N").unpack("C4")
|
310
|
+
raise Error::InvalidCertificate, "invalid cert_len #{cert_len} #{cert_der.inspect}" if unused != 0
|
311
|
+
|
312
|
+
[len1, len2, len3, cert_der].pack("CCC a#{cert_len}")
|
313
|
+
when 1 # precert_entry
|
314
|
+
unless issuer_key_id&.bytesize == 32
|
315
|
+
raise Error::InvalidCertificate,
|
316
|
+
"issuer_key_id must be 32 bytes for precert, given #{issuer_key_id.inspect}"
|
317
|
+
end
|
318
|
+
|
319
|
+
tbs_cert = certificate.tbs_certificate_der
|
320
|
+
tbs_cert_len = tbs_cert.bytesize
|
321
|
+
unused, len1, len2, len3 = [tbs_cert_len].pack("N").unpack("C4")
|
322
|
+
raise Error::InvalidCertificate, "invalid tbs_cert_len #{tbs_cert_len} #{tbs_cert.inspect}" if unused != 0
|
323
|
+
|
324
|
+
[issuer_key_id, len1, len2, len3, tbs_cert].pack("a32 CCC a#{tbs_cert_len}")
|
325
|
+
else
|
326
|
+
raise Error::Unimplemented, "only x509_entry and precert_entry supported, given #{sct[:entry_type].inspect}"
|
327
|
+
end
|
328
|
+
|
329
|
+
[sct.version, 0, sct.timestamp, sct.entry_type, signed_entry, 0].pack(<<~PACK)
|
330
|
+
C # version
|
331
|
+
C # signature_type
|
332
|
+
Q> # timestamp
|
333
|
+
n # entry_type
|
334
|
+
a#{signed_entry.bytesize} # signed_entry
|
335
|
+
n # extensions length
|
336
|
+
PACK
|
337
|
+
end
|
338
|
+
|
339
|
+
def find_issuer_cert(chain)
|
340
|
+
issuer = chain[0]
|
341
|
+
issuer = chain[1] if issuer.preissuer?
|
342
|
+
raise Error::InvalidCertificate, "no issuer certificate found" unless issuer
|
343
|
+
|
344
|
+
issuer
|
345
|
+
end
|
346
|
+
|
347
|
+
def extract_timestamp_from_verification_data(data)
|
348
|
+
# TODO: allow requiring a verified timestamp
|
349
|
+
unless data
|
350
|
+
logger.debug { "no timestamp verification data" }
|
351
|
+
return nil
|
352
|
+
end
|
353
|
+
|
354
|
+
# Checks for https://github.com/ruby/openssl/pull/770
|
355
|
+
if OpenSSL::X509::Store.new.instance_variable_defined?(:@time)
|
356
|
+
logger.warn do
|
357
|
+
"OpenSSL::X509::Store on this version of openssl (#{OpenSSL::VERSION}) does not set time properly, " \
|
358
|
+
"this breaks TSA verification"
|
359
|
+
end
|
360
|
+
return
|
361
|
+
end
|
362
|
+
|
363
|
+
authorities = @timestamp_authorities.map do |ta|
|
364
|
+
store = OpenSSL::X509::Store.new
|
365
|
+
chain = ta.cert_chain.certificates.map do |cert|
|
366
|
+
Internal::X509::Certificate.read(cert.raw_bytes).openssl
|
367
|
+
end
|
368
|
+
chain.each do |cert|
|
369
|
+
store.add_cert(cert)
|
370
|
+
end
|
371
|
+
[ta, chain, store]
|
372
|
+
end
|
373
|
+
|
374
|
+
# https://www.rfc-editor.org/rfc/rfc3161.html#section-2.4.2
|
375
|
+
data.rfc3161_timestamps.map do |ts|
|
376
|
+
resp = OpenSSL::Timestamp::Response.new(ts.signed_timestamp)
|
377
|
+
|
378
|
+
req = OpenSSL::Timestamp::Request.new
|
379
|
+
req.cert_requested = !resp.token.certificates.empty?
|
380
|
+
# TODO: verify the message imprint against the signature in the bundle
|
381
|
+
req.message_imprint = resp.token_info.message_imprint
|
382
|
+
req.algorithm = resp.token_info.algorithm
|
383
|
+
req.policy_id = resp.token_info.policy_id
|
384
|
+
req.nonce = resp.token_info.nonce
|
385
|
+
req.version = resp.token_info.version
|
386
|
+
|
387
|
+
# TODO: verify the hashed message in the message imprint
|
388
|
+
# against the signature in the bundle
|
389
|
+
|
390
|
+
authorities.any? do |ta, chain, store|
|
391
|
+
store.time = resp.token_info.gen_time
|
392
|
+
|
393
|
+
resp.verify(req, store, chain) &&
|
394
|
+
(logger.debug do
|
395
|
+
"timestamp (#{resp.to_text}) verified for #{ta}"
|
396
|
+
end || true)
|
397
|
+
rescue OpenSSL::Timestamp::TimestampError => e
|
398
|
+
logger.error { "timestamp verification failed (#{e})" }
|
399
|
+
false
|
400
|
+
end ||
|
401
|
+
raise(OpenSSL::Timestamp::TimestampError, "timestamp verification failed")
|
402
|
+
resp.token_info.gen_time
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def find_rekor_entry(bundle, hashed_input, offline:)
|
407
|
+
raise Error::InvalidBundle, "multiple tlog entries" if bundle.verification_material.tlog_entries.size > 1
|
408
|
+
|
409
|
+
rekor_entry = bundle.verification_material.tlog_entries&.first
|
410
|
+
has_inclusion_promise = !rekor_entry.nil? && !rekor_entry.inclusion_promise.nil?
|
411
|
+
has_inclusion_proof = !rekor_entry.nil? && !rekor_entry.inclusion_proof&.checkpoint.nil?
|
412
|
+
|
413
|
+
logger.debug do
|
414
|
+
"Looking for rekor entry, " \
|
415
|
+
"has_inclusion_promise=#{has_inclusion_promise} has_inclusion_proof=#{has_inclusion_proof}"
|
416
|
+
end
|
417
|
+
|
418
|
+
expected_entry = bundle.expected_tlog_entry(hashed_input)
|
419
|
+
|
420
|
+
entry = if offline
|
421
|
+
logger.debug { "Offline verification, skipping rekor" }
|
422
|
+
rekor_entry
|
423
|
+
elsif !has_inclusion_proof
|
424
|
+
logger.debug { "No inclusion proof, searching rekor" }
|
425
|
+
@rekor_client.log.entries.retrieve.post(expected_entry)
|
426
|
+
else
|
427
|
+
logger.debug { "Using rekor entry in sigstore bundle" }
|
428
|
+
rekor_entry
|
429
|
+
end
|
430
|
+
|
431
|
+
raise Error::MissingRekorEntry, "Rekor entry not found" unless entry
|
432
|
+
|
433
|
+
logger.debug { "Found rekor entry: #{entry}" }
|
434
|
+
|
435
|
+
actual_body = JSON.parse(entry.canonicalized_body)
|
436
|
+
if bundle.dsse_envelope?
|
437
|
+
# since the hash is over the uncanonicalized envelope, we need to remove it
|
438
|
+
#
|
439
|
+
# NOTE(sigstore-python): This is very slightly weaker than the consistency check
|
440
|
+
# for hashedrekord entries, due to how inclusion is recorded for DSSE:
|
441
|
+
# the included entry for DSSE includes an envelope hash that we
|
442
|
+
# *cannot* verify, since the envelope is uncanonicalized JSON.
|
443
|
+
# Instead, we manually pick apart the entry body below and verify
|
444
|
+
# the parts we can (namely the payload hash and signature list).
|
445
|
+
case actual_body["kind"]
|
446
|
+
when "intoto"
|
447
|
+
actual_body["spec"]["content"].delete("hash")
|
448
|
+
when "dsse"
|
449
|
+
actual_body["spec"].delete("envelopeHash")
|
450
|
+
else
|
451
|
+
raise Error::InvalidRekorEntry, "Unknown kind: #{actual_body["kind"]}"
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
if actual_body != expected_entry
|
456
|
+
require "pp"
|
457
|
+
raise Error::InvalidRekorEntry, "Invalid rekor entry:\n\n" \
|
458
|
+
"Envelope:\n#{bundle.dsse_envelope.pretty_inspect}\n\n" \
|
459
|
+
"Diff:\n#{diff_json(expected_entry, actual_body).pretty_inspect}"
|
460
|
+
end
|
461
|
+
|
462
|
+
entry
|
463
|
+
end
|
464
|
+
|
465
|
+
def diff_json(a, b) # rubocop:disable Naming/MethodParameterName
|
466
|
+
return nil if a == b
|
467
|
+
|
468
|
+
return [a, b] if a.class != b.class
|
469
|
+
|
470
|
+
case a
|
471
|
+
when Hash
|
472
|
+
(a.keys | b.keys).to_h do |k|
|
473
|
+
[k, diff_json(a[k], b[k])]
|
474
|
+
end.compact
|
475
|
+
when Array
|
476
|
+
a.zip(b).filter_map { |x, y| diff_json(x, y) }
|
477
|
+
when String
|
478
|
+
begin
|
479
|
+
da = a.unpack1("m0")
|
480
|
+
db = b.unpack1("m0")
|
481
|
+
|
482
|
+
[{ "decoded" => da, "base64" => a },
|
483
|
+
{ "decoded" => db, "base64" => b }]
|
484
|
+
rescue ArgumentError
|
485
|
+
[a, b]
|
486
|
+
end
|
487
|
+
else
|
488
|
+
[a, b]
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2024 The Sigstore Authors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module Sigstore
|
18
|
+
VERSION = "0.1.1"
|
19
|
+
end
|
data/lib/sigstore.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2024 The Sigstore Authors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module Sigstore
|
18
|
+
class << self
|
19
|
+
attr_writer :logger
|
20
|
+
|
21
|
+
def logger
|
22
|
+
@logger ||= begin
|
23
|
+
require "logger"
|
24
|
+
Logger.new($stderr)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Loggable
|
30
|
+
def logger
|
31
|
+
self.class.logger
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.included(base)
|
35
|
+
base.extend(ClassMethods)
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
def logger
|
40
|
+
Sigstore.logger
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sigstore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- The Sigstore Authors
|
8
|
+
- Samuel Giddins
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2024-10-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: net-http
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: protobug_sigstore_protos
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.1.0
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.1.0
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: uri
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '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'
|
56
|
+
description:
|
57
|
+
email:
|
58
|
+
-
|
59
|
+
- segiddins@segiddins.me
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- CHANGELOG.md
|
65
|
+
- CODEOWNERS
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- data/_store/prod/root.json
|
69
|
+
- data/_store/prod/trusted_root.json
|
70
|
+
- data/_store/staging/root.json
|
71
|
+
- data/_store/staging/trusted_root.json
|
72
|
+
- lib/sigstore.rb
|
73
|
+
- lib/sigstore/error.rb
|
74
|
+
- lib/sigstore/internal/json.rb
|
75
|
+
- lib/sigstore/internal/key.rb
|
76
|
+
- lib/sigstore/internal/keyring.rb
|
77
|
+
- lib/sigstore/internal/merkle.rb
|
78
|
+
- lib/sigstore/internal/set.rb
|
79
|
+
- lib/sigstore/internal/util.rb
|
80
|
+
- lib/sigstore/internal/x509.rb
|
81
|
+
- lib/sigstore/models.rb
|
82
|
+
- lib/sigstore/oidc.rb
|
83
|
+
- lib/sigstore/policy.rb
|
84
|
+
- lib/sigstore/rekor/checkpoint.rb
|
85
|
+
- lib/sigstore/rekor/client.rb
|
86
|
+
- lib/sigstore/signer.rb
|
87
|
+
- lib/sigstore/trusted_root.rb
|
88
|
+
- lib/sigstore/tuf.rb
|
89
|
+
- lib/sigstore/tuf/config.rb
|
90
|
+
- lib/sigstore/tuf/error.rb
|
91
|
+
- lib/sigstore/tuf/file.rb
|
92
|
+
- lib/sigstore/tuf/keys.rb
|
93
|
+
- lib/sigstore/tuf/roles.rb
|
94
|
+
- lib/sigstore/tuf/root.rb
|
95
|
+
- lib/sigstore/tuf/snapshot.rb
|
96
|
+
- lib/sigstore/tuf/targets.rb
|
97
|
+
- lib/sigstore/tuf/timestamp.rb
|
98
|
+
- lib/sigstore/tuf/trusted_metadata_set.rb
|
99
|
+
- lib/sigstore/tuf/updater.rb
|
100
|
+
- lib/sigstore/verifier.rb
|
101
|
+
- lib/sigstore/version.rb
|
102
|
+
homepage: https://github.com/sigstore/sigstore-ruby
|
103
|
+
licenses:
|
104
|
+
- Apache-2.0
|
105
|
+
metadata:
|
106
|
+
allowed_push_host: https://rubygems.org
|
107
|
+
homepage_uri: https://github.com/sigstore/sigstore-ruby
|
108
|
+
rubygems_mfa_required: 'true'
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 3.1.0
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
requirements: []
|
124
|
+
rubygems_version: 3.5.16
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: A pure-ruby implementation of sigstore signature verification
|
128
|
+
test_files: []
|