sigstore 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/CODEOWNERS +6 -0
  4. data/LICENSE +201 -0
  5. data/README.md +26 -0
  6. data/data/_store/prod/root.json +165 -0
  7. data/data/_store/prod/trusted_root.json +114 -0
  8. data/data/_store/staging/root.json +107 -0
  9. data/data/_store/staging/trusted_root.json +87 -0
  10. data/lib/sigstore/error.rb +43 -0
  11. data/lib/sigstore/internal/json.rb +53 -0
  12. data/lib/sigstore/internal/key.rb +183 -0
  13. data/lib/sigstore/internal/keyring.rb +42 -0
  14. data/lib/sigstore/internal/merkle.rb +117 -0
  15. data/lib/sigstore/internal/set.rb +42 -0
  16. data/lib/sigstore/internal/util.rb +52 -0
  17. data/lib/sigstore/internal/x509.rb +460 -0
  18. data/lib/sigstore/models.rb +272 -0
  19. data/lib/sigstore/oidc.rb +149 -0
  20. data/lib/sigstore/policy.rb +104 -0
  21. data/lib/sigstore/rekor/checkpoint.rb +114 -0
  22. data/lib/sigstore/rekor/client.rb +136 -0
  23. data/lib/sigstore/signer.rb +280 -0
  24. data/lib/sigstore/trusted_root.rb +116 -0
  25. data/lib/sigstore/tuf/config.rb +46 -0
  26. data/lib/sigstore/tuf/error.rb +49 -0
  27. data/lib/sigstore/tuf/file.rb +96 -0
  28. data/lib/sigstore/tuf/keys.rb +42 -0
  29. data/lib/sigstore/tuf/roles.rb +106 -0
  30. data/lib/sigstore/tuf/root.rb +53 -0
  31. data/lib/sigstore/tuf/snapshot.rb +45 -0
  32. data/lib/sigstore/tuf/targets.rb +84 -0
  33. data/lib/sigstore/tuf/timestamp.rb +39 -0
  34. data/lib/sigstore/tuf/trusted_metadata_set.rb +193 -0
  35. data/lib/sigstore/tuf/updater.rb +267 -0
  36. data/lib/sigstore/tuf.rb +158 -0
  37. data/lib/sigstore/verifier.rb +492 -0
  38. data/lib/sigstore/version.rb +19 -0
  39. data/lib/sigstore.rb +44 -0
  40. metadata +128 -0
@@ -0,0 +1,460 @@
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 "forwardable"
18
+ require "openssl"
19
+
20
+ module Sigstore
21
+ module Internal
22
+ module X509
23
+ class Certificate
24
+ extend Forwardable
25
+
26
+ attr_reader :openssl
27
+
28
+ def initialize(x509_certificate)
29
+ unless x509_certificate.is_a?(OpenSSL::X509::Certificate)
30
+ raise ArgumentError,
31
+ "Invalid certificate: #{x509_certificate.inspect}"
32
+ end
33
+
34
+ @openssl = x509_certificate
35
+
36
+ raise Error::InvalidCertificate, "invalid X.509 version: #{version.inspect}" if version != 2 # v3
37
+ end
38
+
39
+ def self.read(certificate_bytes)
40
+ new(OpenSSL::X509::Certificate.new(certificate_bytes))
41
+ end
42
+
43
+ def tbs_certificate_der
44
+ if openssl.respond_to?(:tbs_bytes)
45
+ cert = openssl.dup
46
+ short_name = Extension::PrecertificateSignedCertificateTimestamps.oid.short_name
47
+ cert.extensions = cert.extensions.reject! do |ext|
48
+ ext.oid == short_name
49
+ end || raise(Error::InvalidCertificate,
50
+ "No PrecertificateSignedCertificateTimestamps found for the certificate")
51
+ return cert.tbs_bytes
52
+ end
53
+
54
+ extension(Extension::PrecertificateSignedCertificateTimestamps) ||
55
+ raise(Error::InvalidCertificate,
56
+ "No PrecertificateSignedCertificateTimestamps found for the certificate")
57
+
58
+ # This uglyness is needed because there is no way to force modifying an X509 certificate
59
+ # in a way that it will be serialized with the modifications.
60
+ seq = OpenSSL::ASN1.decode(to_der)
61
+ unless seq.is_a?(OpenSSL::ASN1::Sequence) && seq.value.size == 3
62
+ raise Error::InvalidCertificate,
63
+ "invalid X.509 certificate: #{seq.class} #{seq.value.size}"
64
+ end
65
+ seq = seq.value[0]
66
+ unless seq.is_a?(OpenSSL::ASN1::Sequence)
67
+ raise Error::InvalidCertificate,
68
+ "invalid X.509 certificate: #{seq.inspect}"
69
+ end
70
+
71
+ seq.value = seq.value.map! do |v|
72
+ next v unless v.tag == 3
73
+
74
+ v.value = v.value.map! do |v2|
75
+ v2.value = v2.value.map! do |v3|
76
+ next if v3.first.oid == Extension::PrecertificateSignedCertificateTimestamps.oid.oid
77
+
78
+ v3
79
+ end.compact! || raise(Error::InvalidCertificate, "no SCTs found")
80
+ v2
81
+ end
82
+ v
83
+ end
84
+
85
+ seq.to_der
86
+ end
87
+
88
+ def extension(cls)
89
+ openssl.extensions.each do |ext|
90
+ return cls.new(ext) if ext.oid == cls.oid || ext.oid == cls.oid.short_name || ext.oid == cls.oid.oid
91
+ end
92
+ nil
93
+ end
94
+
95
+ def_delegators :openssl, :version, :not_after, :not_before, :to_pem, :to_der,
96
+ :public_key, :to_text, :subject, :hash
97
+
98
+ def ==(other)
99
+ openssl == other.openssl
100
+ end
101
+
102
+ def leaf?
103
+ return false if ca?
104
+
105
+ key_usage = extension(Extension::KeyUsage) ||
106
+ raise(Error::InvalidCertificate,
107
+ "no keyUsage in #{@x509_certificate.extensions.map(&:to_h)}")
108
+
109
+ unless key_usage.digital_signature
110
+ raise Error::InvalidCertificate,
111
+ "invalid certificate for Sigstore purposes: missing digital signature usage: #{key_usage.to_h}"
112
+ end
113
+
114
+ extended_key_usage = extension(Extension::ExtendedKeyUsage)
115
+ return false unless extended_key_usage
116
+
117
+ extended_key_usage.code_signing?
118
+ end
119
+
120
+ def ca?
121
+ basic_constraints = extension(Extension::BasicConstraints)
122
+ return false unless basic_constraints
123
+
124
+ unless basic_constraints.critical?
125
+ raise Error::InvalidCertificate,
126
+ "invalid X.509 certificate: non-critical BasicConstraints in CA"
127
+ end
128
+
129
+ key_usage = extension(Extension::KeyUsage)
130
+ raise Error::InvalidCertificate, "no keyUsage in #{openssl.inspect}" unless key_usage
131
+
132
+ ca = basic_constraints.ca
133
+ key_cert_sign = key_usage.key_cert_sign
134
+
135
+ return true if ca && key_cert_sign
136
+
137
+ return false unless key_cert_sign || ca
138
+
139
+ raise Error::InvalidCertificate,
140
+ "invalid X.509 certificate: inconsistent CA/KeyCertSign in BasicConstraints/KeyUsage " \
141
+ "(#{ca.inspect}, #{key_cert_sign.inspect}):" \
142
+ "\n#{openssl.extensions.map(&:to_h).pretty_inspect}" \
143
+ "\n#{key_usage.pretty_inspect}"
144
+ end
145
+
146
+ def preissuer?
147
+ extended_key_usage = extension(Extension::ExtendedKeyUsage)
148
+ return false unless extended_key_usage
149
+
150
+ extended_key_usage.purposes.include?(OpenSSL::ASN1::ObjectId.new("1.3.6.1.4.1.11129.2.4.4"))
151
+ end
152
+ end
153
+
154
+ class Extension
155
+ class << self
156
+ attr_accessor :oid, :schema
157
+ end
158
+
159
+ def initialize(extension)
160
+ @extension = extension
161
+ value = shift_value([OpenSSL::ASN1.decode(extension.to_der)], OpenSSL::ASN1::Sequence)
162
+ @oid = value.shift
163
+
164
+ unless @extension.is_a?(OpenSSL::X509::Extension) && @oid.oid == self.class.oid.oid
165
+ raise ArgumentError,
166
+ "Invalid extension: #{@extension.inspect} is not a #{@oid.inspect} " \
167
+ "(#{self.class} / #{self.class.oid.inspect})"
168
+ end
169
+
170
+ @critical = false
171
+ @critical = value.shift.value if value.first.is_a?(OpenSSL::ASN1::Boolean)
172
+ raise ArgumentError, "Mis-parsed the critical bit" unless @critical == @extension.critical?
173
+
174
+ contents = shift_value(value, OpenSSL::ASN1::OctetString)
175
+ raise ArgumentError, "Invalid extension: extra fields left in #{self}: #{value}" unless value.empty?
176
+
177
+ parse_value(OpenSSL::ASN1.decode(contents))
178
+ rescue OpenSSL::ASN1::ASN1Error => e
179
+ raise ArgumentError, "Invalid extension: #{e.message} for #{self.class.oid}\n#{extension.inspect}"
180
+ end
181
+
182
+ def critical?
183
+ @extension.critical?
184
+ end
185
+
186
+ def shift_value(value, klass)
187
+ v = value.shift
188
+ raise ArgumentError, "Invalid extension: #{v} is not a #{klass}" unless v.is_a?(klass)
189
+
190
+ v.value
191
+ end
192
+
193
+ def shift_bitstring(value)
194
+ raise ArgumentError, "Invalid bit string: #{value.inspect}" unless value.is_a?(OpenSSL::ASN1::BitString)
195
+
196
+ value.value.each_byte.flat_map do |byte|
197
+ [byte & 0b1000_0000 != 0, byte & 0b0100_0000 != 0, byte & 0b0010_0000 != 0, byte & 0b0001_0000 != 0,
198
+ byte & 0b0000_1000 != 0, byte & 0b0000_0100 != 0, byte & 0b0000_0010 != 0, byte & 0b0000_0001 != 0]
199
+ end[..-value.unused_bits.succ]
200
+ end
201
+
202
+ class SubjectKeyIdentifier < Extension
203
+ attr_reader :key_identifier
204
+
205
+ self.oid = OpenSSL::ASN1::ObjectId.new("2.5.29.14")
206
+
207
+ def parse_value(value)
208
+ unless value.is_a?(OpenSSL::ASN1::OctetString)
209
+ raise ArgumentError,
210
+ "Invalid key identifier: #{value.inspect}"
211
+ end
212
+
213
+ @key_identifier = value.value
214
+ end
215
+ end
216
+
217
+ class KeyUsage < Extension
218
+ self.oid = OpenSSL::ASN1::ObjectId.new("2.5.29.15")
219
+
220
+ attr_reader :digital_signature, :non_repudiation, :key_encipherment, :data_encipherment, :key_agreement,
221
+ :key_cert_sign, :crl_sign, :encipher_only, :decipher_only
222
+
223
+ def parse_value(value)
224
+ @digital_signature, @non_repudiation, @key_encipherment, @data_encipherment, @key_agreement, @key_cert_sign,
225
+ @crl_sign, @encipher_only, @decipher_only =
226
+ shift_bitstring(value)
227
+ end
228
+ end
229
+
230
+ class ExtendedKeyUsage < Extension
231
+ self.oid = OpenSSL::ASN1::ObjectId.new("2.5.29.37")
232
+
233
+ attr_reader :purposes
234
+
235
+ def parse_value(value)
236
+ unless value.is_a?(OpenSSL::ASN1::Sequence)
237
+ rasie ArgumentError,
238
+ "Invalid extended key usage: #{value.inspect}"
239
+ end
240
+
241
+ @purposes = value.value
242
+ return if @purposes.all?(OpenSSL::ASN1::ObjectId)
243
+
244
+ raise ArgumentError,
245
+ "Invalid extended key usage: #{value.inspect}"
246
+ end
247
+
248
+ CODE_SIGNING = OpenSSL::ASN1::ObjectId.new("1.3.6.1.5.5.7.3.3")
249
+
250
+ def code_signing?
251
+ purposes.any? { |purpose| purpose.oid == CODE_SIGNING.oid }
252
+ end
253
+ end
254
+
255
+ class BasicConstraints < Extension
256
+ self.oid = OpenSSL::ASN1::ObjectId.new("2.5.29.19")
257
+
258
+ attr_reader :ca, :path_len_constraint
259
+
260
+ def parse_value(value)
261
+ value = shift_value([value], OpenSSL::ASN1::Sequence)
262
+
263
+ @ca = false
264
+ @path_len_constraint = nil
265
+
266
+ @ca = shift_value(value, OpenSSL::ASN1::Boolean) if value.first.is_a?(OpenSSL::ASN1::Boolean)
267
+
268
+ return unless value.first.is_a?(OpenSSL::ASN1::Integer)
269
+
270
+ @path_len_constraint = shift_value(value, OpenSSL::ASN1::Integer)
271
+ end
272
+ end
273
+
274
+ class SubjectAlternativeName < Extension
275
+ self.oid = OpenSSL::ASN1::ObjectId.new("2.5.29.17")
276
+
277
+ attr_reader :general_names
278
+
279
+ # id-ce-subjectAltName OBJECT IDENTIFIER ::= { id-ce 17 }
280
+
281
+ # SubjectAltName ::= GeneralNames
282
+
283
+ # GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
284
+
285
+ # GeneralName ::= CHOICE {
286
+ # otherName [0] OtherName,
287
+ # rfc822Name [1] IA5String,
288
+ # dNSName [2] IA5String,
289
+ # x400Address [3] ORAddress,
290
+ # directoryName [4] Name,
291
+ # ediPartyName [5] EDIPartyName,
292
+ # uniformResourceIdentifier [6] IA5String,
293
+ # iPAddress [7] OCTET STRING,
294
+ # registeredID [8] OBJECT IDENTIFIER }
295
+
296
+ # OtherName ::= SEQUENCE {
297
+ # type-id OBJECT IDENTIFIER,
298
+ # value [0] EXPLICIT ANY DEFINED BY type-id }
299
+
300
+ # EDIPartyName ::= SEQUENCE {
301
+ # nameAssigner [0] DirectoryString OPTIONAL,
302
+ # partyName [1] DirectoryString }
303
+
304
+ def parse_value(value)
305
+ value = shift_value([value], OpenSSL::ASN1::Sequence)
306
+
307
+ @general_names = value.map do |general_name|
308
+ tag = general_name.tag
309
+
310
+ case tag
311
+ when 1
312
+ [:otherName, general_name.value]
313
+ when 6
314
+ [:uniformResourceIdentifier, general_name.value]
315
+ else
316
+ raise Error::Unimplemented,
317
+ "Unhandled general name tag: #{tag}"
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ class PrecertificateSignedCertificateTimestamps < Extension
324
+ self.oid = OpenSSL::ASN1::ObjectId.new("1.3.6.1.4.1.11129.2.4.2")
325
+
326
+ attr_reader :signed_certificate_timestamps
327
+
328
+ def parse_value(value)
329
+ unless value.is_a?(OpenSSL::ASN1::OctetString)
330
+ raise ArgumentError,
331
+ "Invalid SCT extension: #{value.inspect}"
332
+ end
333
+
334
+ value = value.value
335
+ length = value.unpack1("n")
336
+ value = value.byteslice(2, length)
337
+
338
+ unless value && value.bytesize == length
339
+ raise Error::InvalidCertificate,
340
+ "decoding #{self.class.oid} extension"
341
+ end
342
+
343
+ length = value.unpack1("n")
344
+ value = value.byteslice(2, length)
345
+
346
+ unless value && value.bytesize == length
347
+ raise Error::InvalidCertificate,
348
+ "decoding #{self.class.oid} extension"
349
+ end
350
+
351
+ @signed_certificate_timestamps = unpack_sct_list(value)
352
+ end
353
+
354
+ args = %i[version
355
+ log_id
356
+ timestamp
357
+ extensions_bytes
358
+ hash_algorithm
359
+ signature_algorithm
360
+ entry_type
361
+ signature]
362
+ Timestamp = defined?(Data.define) ? Data.define(*args) : Struct.new(*args, keyword_init: true) # rubocop:disable Naming/ConstantName
363
+
364
+ HASHES = {
365
+ 0 => "none",
366
+ 1 => "md5",
367
+ 2 => "sha1",
368
+ 3 => "sha224",
369
+ 4 => "sha256",
370
+ 5 => "sha384",
371
+ 6 => "sha512",
372
+ 255 => "unknown"
373
+ }.freeze
374
+
375
+ SIGNATURE_ALGORITHMS = {
376
+ 0 => "anonymous",
377
+ 1 => "rsa",
378
+ 2 => "dsa",
379
+ 3 => "ecdsa",
380
+ 255 => "unknown"
381
+ }.freeze
382
+
383
+ private
384
+
385
+ if RUBY_VERSION >= "3.1"
386
+ def unpack_at(string, format, offset:)
387
+ string.unpack(format, offset:)
388
+ end
389
+
390
+ def unpack1_at(string, format, offset:)
391
+ string.unpack1(format, offset:)
392
+ end
393
+ else
394
+ def unpack_at(string, format, offset:)
395
+ string[offset..].unpack(format)
396
+ end
397
+
398
+ def unpack1_at(string, format, offset:)
399
+ string[offset..].unpack1(format)
400
+ end
401
+ end
402
+
403
+ # https://letsencrypt.org/2018/04/04/sct-encoding.html
404
+ def unpack_sct_list(string)
405
+ offset = 0
406
+ len = string.bytesize
407
+ list = []
408
+ while offset < len
409
+ sct_version, sct_log_id, sct_timestamp, sct_extensions_len = unpack_at(string, "Ca32Q>n", offset:)
410
+ offset += 1 + 32 + 8 + 2
411
+ raise Error::Unimplemented, "expect sct version to be 0, got #{sct_version}" unless sct_version.zero?
412
+
413
+ sct_extensions_bytes = unpack1_at(string, "a#{sct_extensions_len}", offset:).b
414
+ offset += sct_extensions_len
415
+
416
+ unless sct_extensions_len.zero?
417
+ raise Error::Unimplemented,
418
+ "sct_extensions_len=#{sct_extensions_len} not supported"
419
+ end
420
+
421
+ sct_signature_alg_hash, sct_signature_alg_sign, sct_signature_len = unpack_at(string, "CCn",
422
+ offset:)
423
+ offset += 1 + 1 + 2
424
+ sct_signature_bytes = unpack1_at(string, "a#{sct_signature_len}", offset:).b
425
+ offset += sct_signature_len
426
+ list << Timestamp.new(
427
+ version: sct_version,
428
+ log_id: sct_log_id.unpack1("H*"),
429
+ timestamp: sct_timestamp,
430
+ hash_algorithm: HASHES.fetch(sct_signature_alg_hash),
431
+ signature_algorithm: SIGNATURE_ALGORITHMS.fetch(sct_signature_alg_sign),
432
+ signature: sct_signature_bytes,
433
+ extensions_bytes: sct_extensions_bytes,
434
+ entry_type: 1 # X509LogEntryType::PRECERTIFICATE
435
+ )
436
+ end
437
+ raise Error::InvalidCertificate, "failed unpacking SCTs: offset=#{offset} len=#{len}" unless offset == len
438
+
439
+ list
440
+ end
441
+ end
442
+
443
+ class FulcioIssuer < Extension
444
+ self.oid = OpenSSL::ASN1::ObjectId.new("1.3.6.1.4.1.57264.1.8")
445
+
446
+ attr_reader :issuer
447
+
448
+ def parse_value(value)
449
+ unless value.is_a?(OpenSSL::ASN1::UTF8String)
450
+ raise ArgumentError,
451
+ "Invalid Fulcio issuer: #{value.inspect}"
452
+ end
453
+
454
+ @issuer = value.value
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end
460
+ end