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
|
@@ -21,7 +21,7 @@ module Sigstore
|
|
|
21
21
|
class Key
|
|
22
22
|
include Loggable
|
|
23
23
|
|
|
24
|
-
def self.from_key_details(key_details, key_bytes)
|
|
24
|
+
def self.from_key_details(key_details, key_bytes, key_id: nil)
|
|
25
25
|
case key_details
|
|
26
26
|
when Common::V1::PublicKeyDetails::PKIX_ECDSA_P256_SHA_256
|
|
27
27
|
key_type = "ecdsa"
|
|
@@ -29,15 +29,34 @@ module Sigstore
|
|
|
29
29
|
when Common::V1::PublicKeyDetails::PKCS1_RSA_PKCS1V5
|
|
30
30
|
key_type = "rsa"
|
|
31
31
|
key_schema = "rsa-pkcs1v15-sha256"
|
|
32
|
+
when Common::V1::PublicKeyDetails::PKIX_ED25519
|
|
33
|
+
# Rekor v2 (tiled) logs sign checkpoints with an Ed25519 key. The raw_bytes
|
|
34
|
+
# are a DER-encoded SubjectPublicKeyInfo, not the bare 32-byte key. The log id
|
|
35
|
+
# is a C2SP signed-note key hash over the log's name, not a digest of the key
|
|
36
|
+
# bytes, so it cannot be recomputed from the key alone; the trusted root must
|
|
37
|
+
# declare it. Without it the key could never match a real entry, so refuse to
|
|
38
|
+
# register an unusable key.
|
|
39
|
+
unless key_id
|
|
40
|
+
raise Error::MissingLogId,
|
|
41
|
+
"Ed25519 (Rekor v2) key has no declared log id; the trusted root must provide log_id.key_id"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return ED25519.new("ed25519", "ed25519", ED25519.public_key_from_spki_der(key_bytes), key_id:)
|
|
32
45
|
else
|
|
33
46
|
# Skip unrecognized key types instead of raising an error.
|
|
34
47
|
# This allows the library to work with newer trusted roots that include
|
|
35
|
-
# key types we don't yet support
|
|
48
|
+
# key types we don't yet support.
|
|
36
49
|
logger.warn { "Skipping unrecognized key type: #{key_details}" }
|
|
37
50
|
return nil
|
|
38
51
|
end
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
# The transparency log declares its own log id in the trusted root; prefer it so
|
|
54
|
+
# the keyring is keyed the same way entries reference the log. For ECDSA/RSA keys
|
|
55
|
+
# the log id is a plain SHA-256 of the key bytes, so it can be recomputed when the
|
|
56
|
+
# trusted root omits it.
|
|
57
|
+
key_id ||= OpenSSL::Digest::SHA256.hexdigest(key_bytes)
|
|
58
|
+
|
|
59
|
+
read(key_type, key_schema, key_bytes, key_id:)
|
|
41
60
|
end
|
|
42
61
|
|
|
43
62
|
def self.read(key_type, schema, key_bytes, key_id: nil)
|
|
@@ -141,8 +160,27 @@ module Sigstore
|
|
|
141
160
|
end
|
|
142
161
|
|
|
143
162
|
class ED25519 < Key
|
|
163
|
+
# jruby-openssl cannot parse or verify Ed25519 keys, so on JRuby we hold a
|
|
164
|
+
# java.security.PublicKey instead of an OpenSSL::PKey and route loading and
|
|
165
|
+
# verification through the JDK (mirroring the java.security path in X509).
|
|
166
|
+
JAVA_ED25519 = RUBY_ENGINE == "jruby"
|
|
167
|
+
|
|
168
|
+
# Load an Ed25519 public key from a DER-encoded SubjectPublicKeyInfo.
|
|
169
|
+
def self.public_key_from_spki_der(der)
|
|
170
|
+
if JAVA_ED25519
|
|
171
|
+
spec = java.security.spec.X509EncodedKeySpec.new(der.to_java_bytes)
|
|
172
|
+
java.security.KeyFactory.getInstance("Ed25519").generatePublic(spec)
|
|
173
|
+
else
|
|
174
|
+
OpenSSL::PKey.read(der)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# +raw+ is the bare 32-byte public key, not a SubjectPublicKeyInfo.
|
|
144
179
|
def self.pkey_from_der(raw)
|
|
145
|
-
if
|
|
180
|
+
if JAVA_ED25519
|
|
181
|
+
# Wrap the raw key in the fixed Ed25519 SubjectPublicKeyInfo prefix.
|
|
182
|
+
public_key_from_spki_der(["302a300506032b6570032100"].pack("H*") + raw)
|
|
183
|
+
elsif OpenSSL::PKey.respond_to?(:new_raw_public_key)
|
|
146
184
|
OpenSSL::PKey.new_raw_public_key("ed25519", raw)
|
|
147
185
|
else
|
|
148
186
|
pem = <<~PEM
|
|
@@ -156,27 +194,54 @@ module Sigstore
|
|
|
156
194
|
|
|
157
195
|
def initialize(...)
|
|
158
196
|
super
|
|
159
|
-
unless @key_type == "ed25519"
|
|
160
|
-
raise ArgumentError,
|
|
161
|
-
"key_type must be ed25519, given #{@key_type}"
|
|
162
|
-
end
|
|
163
|
-
unless @key.is_a?(OpenSSL::PKey::PKey) && @key.oid == "ED25519"
|
|
164
|
-
raise ArgumentError,
|
|
165
|
-
"key must be an OpenSSL::PKey::PKey with oid ED25519, is #{@key.inspect}"
|
|
166
|
-
end
|
|
167
|
-
raise ArgumentError, "schema must be #{schema}" unless @schema == schema
|
|
197
|
+
raise ArgumentError, "key_type must be ed25519, given #{@key_type}" unless @key_type == "ed25519"
|
|
168
198
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
if JAVA_ED25519
|
|
200
|
+
unless @key.respond_to?(:getAlgorithm) && %w[Ed25519 EdDSA].include?(@key.getAlgorithm)
|
|
201
|
+
raise ArgumentError, "key must be a java Ed25519 PublicKey, is #{@key.inspect}"
|
|
202
|
+
end
|
|
203
|
+
elsif !(@key.is_a?(OpenSSL::PKey::PKey) && @key.oid == "ED25519")
|
|
204
|
+
raise ArgumentError, "key must be an OpenSSL::PKey::PKey with oid ED25519, is #{@key.inspect}"
|
|
174
205
|
end
|
|
206
|
+
|
|
207
|
+
raise ArgumentError, "Unsupported schema #{schema}" unless @schema == "ed25519"
|
|
175
208
|
end
|
|
176
209
|
|
|
177
210
|
def verify(_algo, signature, data)
|
|
211
|
+
return java_verify(signature, data) if JAVA_ED25519
|
|
212
|
+
|
|
178
213
|
super(nil, signature, data)
|
|
179
214
|
end
|
|
215
|
+
|
|
216
|
+
# Ed25519 keys do not implement #to_der; the SubjectPublicKeyInfo DER is the
|
|
217
|
+
# public encoding.
|
|
218
|
+
def to_der
|
|
219
|
+
return String.from_java_bytes(@key.getEncoded).b if JAVA_ED25519
|
|
220
|
+
|
|
221
|
+
@key.public_to_der
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def public_to_der
|
|
225
|
+
to_der
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def to_pem
|
|
229
|
+
return super unless JAVA_ED25519
|
|
230
|
+
|
|
231
|
+
"-----BEGIN PUBLIC KEY-----\n#{Internal::Util.base64_encode(to_der)}\n-----END PUBLIC KEY-----\n"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def java_verify(signature, data)
|
|
237
|
+
sig = java.security.Signature.getInstance("Ed25519")
|
|
238
|
+
sig.initVerify(@key)
|
|
239
|
+
sig.update(data.to_java_bytes)
|
|
240
|
+
sig.verify(signature.to_java_bytes)
|
|
241
|
+
rescue java.security.GeneralSecurityException => e
|
|
242
|
+
logger.debug { "Ed25519 verification failed: #{e}" }
|
|
243
|
+
false
|
|
244
|
+
end
|
|
180
245
|
end
|
|
181
246
|
end
|
|
182
247
|
end
|
|
@@ -69,7 +69,7 @@ module Sigstore
|
|
|
69
69
|
|
|
70
70
|
intermediate_result = chain_inner(
|
|
71
71
|
leaf_hash,
|
|
72
|
-
|
|
72
|
+
proof[...inner] || raise(MissingHashError, "missing left hashes"),
|
|
73
73
|
log_index
|
|
74
74
|
)
|
|
75
75
|
|
|
@@ -93,7 +93,7 @@ module Sigstore
|
|
|
93
93
|
|
|
94
94
|
def self.chain_inner(seed, hashes, log_index)
|
|
95
95
|
hashes.each_with_index do |hash, i|
|
|
96
|
-
seed = if (
|
|
96
|
+
seed = if (log_index >> i).nobits?(1)
|
|
97
97
|
hash_children(seed, hash)
|
|
98
98
|
else
|
|
99
99
|
hash_children(hash, seed)
|
|
@@ -47,6 +47,13 @@ module Sigstore
|
|
|
47
47
|
def base64_decode(string)
|
|
48
48
|
string.unpack1("m0")
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
# The DSSE Pre-Authentication Encoding over a payload and its type, as
|
|
52
|
+
# defined by the DSSE v1 spec. This is the byte string that is signed (and
|
|
53
|
+
# verified) for a DSSE envelope.
|
|
54
|
+
def dsse_pae(payload_type, payload)
|
|
55
|
+
"DSSEv1 #{payload_type.bytesize} #{payload_type} #{payload.bytesize} #{payload}".b
|
|
56
|
+
end
|
|
50
57
|
end
|
|
51
58
|
end
|
|
52
59
|
end
|
|
@@ -111,7 +111,12 @@ module Sigstore
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def tbs_certificate_der
|
|
114
|
-
|
|
114
|
+
# jruby-openssl < 0.19.0's X509::Certificate#tbs_bytes does not preserve
|
|
115
|
+
# extension order after reassigning extensions, yielding the wrong TBS and
|
|
116
|
+
# breaking SCT verification. Fall through to the manual ASN.1 path there,
|
|
117
|
+
# which manipulates the DER directly and is engine-independent.
|
|
118
|
+
if openssl.respond_to?(:tbs_bytes) &&
|
|
119
|
+
(RUBY_ENGINE != "jruby" || Gem::Version.new(JOpenSSL::VERSION) >= Gem::Version.new("0.19.0"))
|
|
115
120
|
cert = openssl.dup
|
|
116
121
|
short_name = Extension::PrecertificateSignedCertificateTimestamps.oid.short_name
|
|
117
122
|
cert.extensions = cert.extensions.reject! do |ext|
|
|
@@ -438,7 +443,7 @@ module Sigstore
|
|
|
438
443
|
signature_algorithm
|
|
439
444
|
entry_type
|
|
440
445
|
signature]
|
|
441
|
-
Timestamp = defined?(Data.define) ? Data.define(*args) : Struct.new(*args
|
|
446
|
+
Timestamp = defined?(Data.define) ? Data.define(*args) : Struct.new(*args) # rubocop:disable Naming/ConstantName
|
|
442
447
|
|
|
443
448
|
HASHES = {
|
|
444
449
|
0 => "none",
|
|
@@ -474,11 +479,6 @@ module Sigstore
|
|
|
474
479
|
sct_extensions_bytes = string.unpack1("a#{sct_extensions_len}", offset:).b
|
|
475
480
|
offset += sct_extensions_len
|
|
476
481
|
|
|
477
|
-
unless sct_extensions_len.zero?
|
|
478
|
-
raise Error::Unimplemented,
|
|
479
|
-
"sct_extensions_len=#{sct_extensions_len} not supported"
|
|
480
|
-
end
|
|
481
|
-
|
|
482
482
|
sct_signature_alg_hash, sct_signature_alg_sign, sct_signature_len = string.unpack("CCn", offset:)
|
|
483
483
|
offset += 1 + 1 + 2
|
|
484
484
|
sct_signature_bytes = string.unpack1("a#{sct_signature_len}", offset:).b
|
data/lib/sigstore/models.rb
CHANGED
|
@@ -19,7 +19,7 @@ require_relative "error"
|
|
|
19
19
|
require_relative "trusted_root"
|
|
20
20
|
|
|
21
21
|
module Sigstore
|
|
22
|
-
VerificationResult = Struct.new(:success
|
|
22
|
+
VerificationResult = Struct.new(:success) do
|
|
23
23
|
# @implements VerificationResult
|
|
24
24
|
|
|
25
25
|
alias_method :verified?, :success
|
|
@@ -92,6 +92,16 @@ module Sigstore
|
|
|
92
92
|
raise Error::InvalidVerificationInput, "bundle with message_signature requires an artifact"
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
@hashed_input = self.class.hashed_input_for(artifact)
|
|
96
|
+
|
|
97
|
+
freeze
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Derive the SHA2-256 HashOutput the verifier checks signatures and Rekor
|
|
101
|
+
# entries against, from any of the Artifact oneof variants: the raw bytes
|
|
102
|
+
# (:artifact), a "sha256:"-prefixed URI (:artifact_uri), or a typed digest
|
|
103
|
+
# (:artifact_digest, protobuf-specs v0.5.1+).
|
|
104
|
+
def self.hashed_input_for(artifact)
|
|
95
105
|
case artifact.data
|
|
96
106
|
when :artifact_uri
|
|
97
107
|
unless artifact.artifact_uri.start_with?("sha256:")
|
|
@@ -99,26 +109,40 @@ module Sigstore
|
|
|
99
109
|
"artifact_uri must be prefixed with 'sha256:'"
|
|
100
110
|
end
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
Common::V1::HashOutput.new.tap do |hash_output|
|
|
103
113
|
hash_output.algorithm = Common::V1::HashAlgorithm::SHA2_256
|
|
104
114
|
hexdigest = artifact.artifact_uri.split(":", 2).last
|
|
105
115
|
hash_output.digest = Internal::Util.hex_decode(hexdigest)
|
|
106
116
|
end
|
|
107
117
|
when :artifact
|
|
108
|
-
|
|
118
|
+
Common::V1::HashOutput.new.tap do |hash_output|
|
|
109
119
|
hash_output.algorithm = Common::V1::HashAlgorithm::SHA2_256
|
|
110
120
|
hash_output.digest = OpenSSL::Digest.new("SHA256").update(artifact.artifact).digest
|
|
111
121
|
end
|
|
122
|
+
when :artifact_digest
|
|
123
|
+
# The rest of the pipeline (message-signature and Rekor digest checks)
|
|
124
|
+
# operates on SHA2-256, so reject other algorithms.
|
|
125
|
+
artifact.artifact_digest.tap do |hash_output|
|
|
126
|
+
unless hash_output.algorithm == Common::V1::HashAlgorithm::SHA2_256
|
|
127
|
+
raise Error::InvalidVerificationInput,
|
|
128
|
+
"unsupported artifact digest algorithm: #{hash_output.algorithm}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
112
131
|
else
|
|
113
132
|
raise Error::InvalidVerificationInput, "Unsupported artifact data: #{artifact.data}"
|
|
114
133
|
end
|
|
115
|
-
|
|
116
|
-
freeze
|
|
117
134
|
end
|
|
118
135
|
end
|
|
119
136
|
|
|
120
137
|
class SBundle < DelegateClass(Bundle::V1::Bundle)
|
|
121
|
-
attr_reader :bundle_type, :leaf_certificate
|
|
138
|
+
attr_reader :bundle_type, :leaf_certificate, :signing_key_hint
|
|
139
|
+
|
|
140
|
+
# A bundle whose signing identity is a bare public key (a "managed" /
|
|
141
|
+
# bring-your-own-key signature) rather than a Fulcio-issued certificate.
|
|
142
|
+
# The key itself is supplied out-of-band; the bundle only carries a hint.
|
|
143
|
+
def key_based?
|
|
144
|
+
!@signing_key_hint.nil?
|
|
145
|
+
end
|
|
122
146
|
|
|
123
147
|
def initialize(*)
|
|
124
148
|
super
|
|
@@ -138,11 +162,24 @@ module Sigstore
|
|
|
138
162
|
new(bundle)
|
|
139
163
|
end
|
|
140
164
|
|
|
141
|
-
|
|
165
|
+
# +verifier_pem+ is the PEM of the public key the entry should be bound to. For
|
|
166
|
+
# certificate bundles it defaults to the leaf certificate's PEM; for managed-key
|
|
167
|
+
# bundles the caller passes the supplied key's SubjectPublicKeyInfo PEM.
|
|
168
|
+
def expected_tlog_entry(hashed_input, verifier_pem = leaf_certificate&.to_pem)
|
|
142
169
|
case content
|
|
143
170
|
when :message_signature
|
|
144
|
-
expected_hashed_rekord_tlog_entry(hashed_input)
|
|
171
|
+
expected_hashed_rekord_tlog_entry(hashed_input, verifier_pem)
|
|
145
172
|
when :dsse_envelope
|
|
173
|
+
# The DSSE-v1 (Rekor v1) expected-entry builders embed the signing leaf
|
|
174
|
+
# certificate. Key-based (managed-key) DSSE is only supported on Rekor v2, whose
|
|
175
|
+
# consistency check runs before this method; reaching here without a leaf
|
|
176
|
+
# certificate means a key-based bundle carries a v1 DSSE entry, which we cannot
|
|
177
|
+
# build an expected entry for. Fail cleanly rather than dereferencing a nil cert.
|
|
178
|
+
if leaf_certificate.nil?
|
|
179
|
+
raise Error::InvalidBundle,
|
|
180
|
+
"key-based DSSE bundles are only supported with Rekor v2 entries"
|
|
181
|
+
end
|
|
182
|
+
|
|
146
183
|
rekor_entry = verification_material.tlog_entries.first
|
|
147
184
|
canonicalized_body = begin
|
|
148
185
|
JSON.parse(rekor_entry.canonicalized_body)
|
|
@@ -152,9 +189,9 @@ module Sigstore
|
|
|
152
189
|
|
|
153
190
|
case kind_version = canonicalized_body.values_at("kind", "apiVersion")
|
|
154
191
|
when %w[dsse 0.0.1]
|
|
155
|
-
expected_dsse_0_0_1_tlog_entry
|
|
192
|
+
expected_dsse_0_0_1_tlog_entry(verifier_pem)
|
|
156
193
|
when %w[intoto 0.0.2]
|
|
157
|
-
expected_intoto_0_0_2_tlog_entry
|
|
194
|
+
expected_intoto_0_0_2_tlog_entry(verifier_pem)
|
|
158
195
|
else
|
|
159
196
|
raise Error::InvalidRekorEntry, "Unhandled rekor entry kind/version: #{kind_version.inspect}"
|
|
160
197
|
end
|
|
@@ -193,7 +230,10 @@ module Sigstore
|
|
|
193
230
|
|
|
194
231
|
case verification_material.content
|
|
195
232
|
when :public_key
|
|
196
|
-
|
|
233
|
+
# Managed key: the verifying key is provided out-of-band; the bundle only
|
|
234
|
+
# carries a hint identifying it. There is no certificate to anchor.
|
|
235
|
+
@signing_key_hint = verification_material.public_key.hint
|
|
236
|
+
return
|
|
197
237
|
when :x509_certificate_chain
|
|
198
238
|
certs = verification_material.x509_certificate_chain.certificates.map do |cert|
|
|
199
239
|
Internal::X509::Certificate.read(cert.raw_bytes)
|
|
@@ -211,13 +251,13 @@ module Sigstore
|
|
|
211
251
|
raise Error::InvalidBundle, "expected certificate to be leaf" unless @leaf_certificate.leaf?
|
|
212
252
|
end
|
|
213
253
|
|
|
214
|
-
def expected_hashed_rekord_tlog_entry(hashed_input)
|
|
254
|
+
def expected_hashed_rekord_tlog_entry(hashed_input, verifier_pem)
|
|
215
255
|
{
|
|
216
256
|
"spec" => {
|
|
217
257
|
"signature" => {
|
|
218
258
|
"content" => Internal::Util.base64_encode(message_signature.signature),
|
|
219
259
|
"publicKey" => {
|
|
220
|
-
"content" => Internal::Util.base64_encode(
|
|
260
|
+
"content" => Internal::Util.base64_encode(verifier_pem)
|
|
221
261
|
}
|
|
222
262
|
},
|
|
223
263
|
"data" => {
|
|
@@ -232,7 +272,7 @@ module Sigstore
|
|
|
232
272
|
}
|
|
233
273
|
end
|
|
234
274
|
|
|
235
|
-
def expected_intoto_0_0_2_tlog_entry
|
|
275
|
+
def expected_intoto_0_0_2_tlog_entry(_verifier_pem)
|
|
236
276
|
{
|
|
237
277
|
"apiVersion" => "0.0.2",
|
|
238
278
|
"kind" => "intoto",
|
|
@@ -263,7 +303,7 @@ module Sigstore
|
|
|
263
303
|
}
|
|
264
304
|
end
|
|
265
305
|
|
|
266
|
-
def expected_dsse_0_0_1_tlog_entry
|
|
306
|
+
def expected_dsse_0_0_1_tlog_entry(verifier_pem)
|
|
267
307
|
{
|
|
268
308
|
"apiVersion" => "0.0.1",
|
|
269
309
|
"kind" => "dsse",
|
|
@@ -276,7 +316,7 @@ module Sigstore
|
|
|
276
316
|
dsse_envelope.signatures.map do |sig|
|
|
277
317
|
{
|
|
278
318
|
"signature" => Internal::Util.base64_encode(sig.sig),
|
|
279
|
-
"verifier" => Internal::Util.base64_encode(
|
|
319
|
+
"verifier" => Internal::Util.base64_encode(verifier_pem)
|
|
280
320
|
}
|
|
281
321
|
end
|
|
282
322
|
}
|
data/lib/sigstore/oidc.rb
CHANGED
|
@@ -123,6 +123,9 @@ module Sigstore
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def validate_nbf(nbf, now, leeway)
|
|
126
|
+
# The nbf ("not before") claim is optional (RFC 7519); skip validation when absent.
|
|
127
|
+
return if nbf.nil?
|
|
128
|
+
|
|
126
129
|
raise Error::InvalidIdentityToken, "nbf claim must be an integer" unless nbf.is_a?(Integer)
|
|
127
130
|
raise Error::InvalidIdentityToken, "nbf claim is in the future" if nbf > now + leeway
|
|
128
131
|
end
|
data/lib/sigstore/policy.rb
CHANGED
|
@@ -87,6 +87,14 @@ module Sigstore
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# No identity to check: used for managed-key (bring-your-own-key) verification,
|
|
91
|
+
# where trust comes from the supplied key rather than a certificate identity.
|
|
92
|
+
class UnsafeNoOp
|
|
93
|
+
def verify(_cert)
|
|
94
|
+
VerificationSuccess.new
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
90
98
|
class Identity
|
|
91
99
|
def initialize(identity:, issuer:)
|
|
92
100
|
@identity = identity
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
module Sigstore
|
|
18
18
|
module Rekor
|
|
19
19
|
module Checkpoint
|
|
20
|
-
Signature = Struct.new(:name, :sig_hash, :signature
|
|
20
|
+
Signature = Struct.new(:name, :sig_hash, :signature)
|
|
21
21
|
|
|
22
|
-
SignedCheckpoint = Struct.new(:signed_note, :checkpoint
|
|
22
|
+
SignedCheckpoint = Struct.new(:signed_note, :checkpoint) do
|
|
23
23
|
# @implements SignedCheckpoint
|
|
24
24
|
|
|
25
25
|
def self.from_text(text)
|
|
@@ -30,7 +30,7 @@ module Sigstore
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
SignedNote = Struct.new(:note, :signatures
|
|
33
|
+
SignedNote = Struct.new(:note, :signatures) do
|
|
34
34
|
# @implements SignedNote
|
|
35
35
|
|
|
36
36
|
def self.from_text(text)
|
|
@@ -65,19 +65,29 @@ module Sigstore
|
|
|
65
65
|
|
|
66
66
|
def verify(rekor_keyring, key_id)
|
|
67
67
|
data = note.encode("utf-8")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
key_hint = key_id[0, 4]
|
|
69
|
+
|
|
70
|
+
# A checkpoint may also carry cosignatures from witnesses whose verification
|
|
71
|
+
# keys we do not yet know; their key hints differ from the log's, and the
|
|
72
|
+
# signed-note / Rekor v2 spec says to ignore those lines. We require that at
|
|
73
|
+
# least one signature made with the log's own key verifies.
|
|
74
|
+
candidates = signatures.select { |signature| signature.sig_hash == key_hint }
|
|
75
|
+
raise Error::InvalidCheckpoint, "no checkpoint signature matching the log key" if candidates.empty?
|
|
76
|
+
|
|
77
|
+
# Keyring#verify raises KeyError when the log key id is not in the keyring (an
|
|
78
|
+
# untrusted or unknown log); treat that as a non-verifying candidate so it fails
|
|
79
|
+
# closed via the check below rather than escaping as an uncaught exception.
|
|
80
|
+
verified = candidates.any? do |signature|
|
|
75
81
|
rekor_keyring.verify(key_id: key_id.unpack1("H*"), signature: signature.signature, data:)
|
|
82
|
+
rescue Error::InvalidSignature, KeyError
|
|
83
|
+
false
|
|
76
84
|
end
|
|
85
|
+
|
|
86
|
+
raise Error::InvalidCheckpoint, "no valid checkpoint signature from the log key" unless verified
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
|
|
80
|
-
LogCheckpoint = Struct.new(:origin, :log_size, :log_hash, :other_content
|
|
90
|
+
LogCheckpoint = Struct.new(:origin, :log_size, :log_hash, :other_content) do
|
|
81
91
|
# @implements LogCheckpoint
|
|
82
92
|
|
|
83
93
|
def self.from_text(text)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
17
|
require "net/http"
|
|
18
|
+
require_relative "../version"
|
|
18
19
|
|
|
19
20
|
module Sigstore
|
|
20
21
|
module Rekor
|
|
@@ -43,6 +44,39 @@ module Sigstore
|
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
# Client for the Rekor v2 (tiled / rekor-tiles) API. Unlike v1, creating an
|
|
48
|
+
# entry returns a fully-formed protobuf TransparencyLogEntry (including the
|
|
49
|
+
# inclusion proof and checkpoint) directly, so there is no separate retrieval
|
|
50
|
+
# step. See https://github.com/sigstore/rekor-tiles/blob/main/CLIENTS.md
|
|
51
|
+
class V2Client
|
|
52
|
+
def initialize(url:)
|
|
53
|
+
@url = URI.join("#{url.chomp("/")}/", "api/v2/")
|
|
54
|
+
|
|
55
|
+
net = defined?(Gem::Net) ? Gem::Net : Net
|
|
56
|
+
@session = net::HTTP.new(@url.host, @url.port)
|
|
57
|
+
@session.use_ssl = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Submit a proposed entry (a CreateEntryRequest, as a Hash) and return the
|
|
61
|
+
# integrated V1::TransparencyLogEntry. The log only responds once the entry
|
|
62
|
+
# has been included, so this call can take a while.
|
|
63
|
+
def create_entry(payload)
|
|
64
|
+
resp = @session.post2(URI.join(@url, "log/entries").path, payload.to_json,
|
|
65
|
+
{ "Content-Type" => "application/json", "Accept" => "application/json",
|
|
66
|
+
"User-Agent" => Sigstore::USER_AGENT })
|
|
67
|
+
|
|
68
|
+
unless %w[200 201].include?(resp.code)
|
|
69
|
+
raise Error::FailedRekorPost,
|
|
70
|
+
"#{resp.code} #{resp.message.inspect}\n#{JSON.pretty_generate(payload)}\n#{resp.body}"
|
|
71
|
+
end
|
|
72
|
+
unless resp.content_type == "application/json"
|
|
73
|
+
raise Error::FailedRekorPost, "Unexpected content type: #{resp.content_type.inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
V1::TransparencyLogEntry.decode_json(resp.body, registry: REGISTRY)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
46
80
|
class Log
|
|
47
81
|
def initialize(url, session:)
|
|
48
82
|
@url = url
|
|
@@ -66,7 +100,8 @@ module Sigstore
|
|
|
66
100
|
|
|
67
101
|
def post(entry)
|
|
68
102
|
resp = @session.post2(@url.path.chomp("/"), entry.to_json,
|
|
69
|
-
{ "Content-Type" => "application/json", "Accept" => "application/json"
|
|
103
|
+
{ "Content-Type" => "application/json", "Accept" => "application/json",
|
|
104
|
+
"User-Agent" => Sigstore::USER_AGENT })
|
|
70
105
|
|
|
71
106
|
unless resp.code == "201"
|
|
72
107
|
raise Error::FailedRekorPost,
|
|
@@ -89,7 +124,8 @@ module Sigstore
|
|
|
89
124
|
def post(expected_entry)
|
|
90
125
|
data = { entries: [expected_entry] }
|
|
91
126
|
resp = @session.post2(@url.path, data.to_json,
|
|
92
|
-
{ "Content-Type" => "application/json", "Accept" => "application/json"
|
|
127
|
+
{ "Content-Type" => "application/json", "Accept" => "application/json",
|
|
128
|
+
"User-Agent" => Sigstore::USER_AGENT })
|
|
93
129
|
|
|
94
130
|
if resp.code != "200"
|
|
95
131
|
raise Error::FailedRekorLookup,
|