sigstore 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,136 @@
|
|
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 "net/http"
|
18
|
+
|
19
|
+
module Sigstore
|
20
|
+
module Rekor
|
21
|
+
class Client
|
22
|
+
DEFAULT_REKOR_URL = "https://rekor.sigstore.dev"
|
23
|
+
STAGING_REKOR_URL = "https://rekor.sigstage.dev"
|
24
|
+
|
25
|
+
def initialize(url:)
|
26
|
+
@url = URI.join(url, "api/v1/")
|
27
|
+
|
28
|
+
net = defined?(Gem::Net) ? Gem::Net : Net
|
29
|
+
@session = net::HTTP.new(@url.host, @url.port)
|
30
|
+
@session.use_ssl = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.production
|
34
|
+
new(url: DEFAULT_REKOR_URL)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.staging
|
38
|
+
new(url: STAGING_REKOR_URL)
|
39
|
+
end
|
40
|
+
|
41
|
+
def log
|
42
|
+
Log.new(URI.join(@url, "log/"), session: @session)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Log
|
47
|
+
def initialize(url, session:)
|
48
|
+
@url = url
|
49
|
+
@session = session
|
50
|
+
end
|
51
|
+
|
52
|
+
def entries
|
53
|
+
Entries.new(URI.join(@url, "entries/"), session: @session)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Entries
|
58
|
+
def initialize(url, session:)
|
59
|
+
@url = url
|
60
|
+
@session = session
|
61
|
+
end
|
62
|
+
|
63
|
+
def retrieve
|
64
|
+
Retrieve.new(URI.join(@url, "retrieve/"), session: @session)
|
65
|
+
end
|
66
|
+
|
67
|
+
def post(entry)
|
68
|
+
resp = @session.post2(@url.path.chomp("/"), entry.to_json,
|
69
|
+
{ "Content-Type" => "application/json", "Accept" => "application/json" })
|
70
|
+
|
71
|
+
unless resp.code == "201"
|
72
|
+
raise Error::FailedRekorPost,
|
73
|
+
"#{resp.code} #{resp.message.inspect}\n#{JSON.pretty_generate(entry)}\n#{resp.body}"
|
74
|
+
end
|
75
|
+
unless resp.content_type == "application/json"
|
76
|
+
raise Error::FailedRekorPost, "Unexpected content type: #{resp.content_type.inspect}"
|
77
|
+
end
|
78
|
+
|
79
|
+
body = JSON.parse(resp.body)
|
80
|
+
Entries.decode_transparency_log_entry(body)
|
81
|
+
end
|
82
|
+
|
83
|
+
class Retrieve
|
84
|
+
def initialize(url, session:)
|
85
|
+
@url = url
|
86
|
+
@session = session
|
87
|
+
end
|
88
|
+
|
89
|
+
def post(expected_entry)
|
90
|
+
data = { entries: [expected_entry] }
|
91
|
+
resp = @session.post2(@url.path, data.to_json,
|
92
|
+
{ "Content-Type" => "application/json", "Accept" => "application/json" })
|
93
|
+
|
94
|
+
if resp.code != "200"
|
95
|
+
raise Error::FailedRekorLookup,
|
96
|
+
"#{resp.code} #{resp.message.inspect}\n#{JSON.pretty_generate(data)}\n#{resp.body}"
|
97
|
+
end
|
98
|
+
|
99
|
+
results = JSON.parse(resp.body)
|
100
|
+
|
101
|
+
results.map do |result|
|
102
|
+
Entries.decode_transparency_log_entry(result)
|
103
|
+
end.min_by(&:integrated_time)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.decode_transparency_log_entry(response)
|
108
|
+
raise ArgumentError, "response must be a Hash" unless response.is_a?(Hash)
|
109
|
+
raise ArgumentError, "Received multiple entries in response" if response.size != 1
|
110
|
+
|
111
|
+
_, result = response.first
|
112
|
+
entry = V1::TransparencyLogEntry.new
|
113
|
+
entry.canonicalized_body = Internal::Util.base64_decode(result.fetch("body"))
|
114
|
+
entry.integrated_time = result.fetch("integratedTime")
|
115
|
+
entry.log_id = Common::V1::LogId.new
|
116
|
+
entry.log_id.key_id = Internal::Util.hex_decode(result.fetch("logID"))
|
117
|
+
entry.log_index = result.fetch("logIndex")
|
118
|
+
if (set = result.dig("verification", "signedEntryTimestamp"))
|
119
|
+
entry.inclusion_promise = V1::InclusionPromise.new
|
120
|
+
entry.inclusion_promise.signed_entry_timestamp = Internal::Util.base64_decode(set)
|
121
|
+
end
|
122
|
+
if (inclusion_proof = result.dig("verification", "inclusionProof"))
|
123
|
+
entry.inclusion_proof = V1::InclusionProof.new
|
124
|
+
entry.inclusion_proof.checkpoint = V1::Checkpoint.new
|
125
|
+
entry.inclusion_proof.checkpoint.envelope = inclusion_proof.fetch("checkpoint")
|
126
|
+
entry.inclusion_proof.hashes = inclusion_proof.fetch("hashes").map { |h| Internal::Util.hex_decode(h) }
|
127
|
+
entry.inclusion_proof.log_index = inclusion_proof.fetch("logIndex")
|
128
|
+
entry.inclusion_proof.root_hash = Internal::Util.hex_decode(inclusion_proof.fetch("rootHash"))
|
129
|
+
entry.inclusion_proof.tree_size = inclusion_proof.fetch("treeSize")
|
130
|
+
end
|
131
|
+
|
132
|
+
entry
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,280 @@
|
|
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 "internal/util"
|
18
|
+
require_relative "internal/x509"
|
19
|
+
require_relative "models"
|
20
|
+
require_relative "oidc"
|
21
|
+
require_relative "policy"
|
22
|
+
require_relative "verifier"
|
23
|
+
|
24
|
+
module Sigstore
|
25
|
+
class Signer
|
26
|
+
include Loggable
|
27
|
+
|
28
|
+
def initialize(jwt:, trusted_root:)
|
29
|
+
@identity_token = OIDC::IdentityToken.new(jwt)
|
30
|
+
@trusted_root = trusted_root
|
31
|
+
|
32
|
+
@verifier = Verifier.for_trust_root(trust_root: @trusted_root)
|
33
|
+
end
|
34
|
+
|
35
|
+
def sign(payload)
|
36
|
+
# 2) generate a keypair
|
37
|
+
keypair = generate_keypair
|
38
|
+
# 3) generate a CreateSigningCertificateRequest
|
39
|
+
csr = generate_csr(keypair)
|
40
|
+
# 4) get a cert chain from fulcio
|
41
|
+
leaf = fetch_cert(csr)
|
42
|
+
# 5) verify returned cert chain
|
43
|
+
verify_chain(leaf)
|
44
|
+
# 6) sign the payload
|
45
|
+
signature = sign_payload(payload, keypair)
|
46
|
+
# 7) send hash of signature to timestamping service
|
47
|
+
timestamp_verification_data = submit_signature_hash_to_timstamping_service(signature)
|
48
|
+
# 8) submit signed metadata to transparency service
|
49
|
+
hashed_input = Common::V1::HashOutput.new
|
50
|
+
hashed_input.algorithm = Common::V1::HashAlgorithm::SHA2_256
|
51
|
+
hashed_input.digest = OpenSSL::Digest("SHA256").digest(payload)
|
52
|
+
tlog_entry = submit_signed_metadata_to_transparency_service(signature, leaf, hashed_input)
|
53
|
+
# 9) perform verification
|
54
|
+
|
55
|
+
bundle = collect_bundle(leaf, [tlog_entry], timestamp_verification_data, hashed_input, signature)
|
56
|
+
verify(payload, bundle)
|
57
|
+
|
58
|
+
bundle
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def generate_keypair
|
64
|
+
# maybe allow configuring?
|
65
|
+
key = OpenSSL::PKey::EC.generate("prime256v1")
|
66
|
+
logger.debug { "Generated keypair #{key}" }
|
67
|
+
key
|
68
|
+
end
|
69
|
+
|
70
|
+
def generate_csr(keypair)
|
71
|
+
csr = OpenSSL::X509::Request.new
|
72
|
+
|
73
|
+
# The subject is unused, but must be set to avoid an error on JRuby
|
74
|
+
csr.subject = OpenSSL::X509::Name.new
|
75
|
+
csr.public_key = keypair
|
76
|
+
|
77
|
+
# The subject in the CertificationRequestInfo is an X.501 RelativeDistinguishedName.
|
78
|
+
# The value of the RelativeDistinguishedName SHOULD be the subject of the authentication token;
|
79
|
+
# its type MUST be the type identified in the Fulcio instance’s public configuration.
|
80
|
+
# NOTE: the subject of the CSR is unused
|
81
|
+
|
82
|
+
extension = OpenSSL::X509::ExtensionFactory.new.create_extension(
|
83
|
+
"basicConstraints",
|
84
|
+
"CA:FALSE",
|
85
|
+
true # critical
|
86
|
+
)
|
87
|
+
csr.add_attribute OpenSSL::X509::Attribute.new(
|
88
|
+
"extReq",
|
89
|
+
OpenSSL::ASN1::Set.new(
|
90
|
+
[OpenSSL::ASN1::Sequence.new([extension])]
|
91
|
+
)
|
92
|
+
)
|
93
|
+
|
94
|
+
csr.sign keypair, OpenSSL::Digest.new("SHA256")
|
95
|
+
|
96
|
+
logger.debug { "Generated CSR" }
|
97
|
+
|
98
|
+
{
|
99
|
+
credentials: {
|
100
|
+
oidc_identity_token: @identity_token.raw_token
|
101
|
+
},
|
102
|
+
certificate_signing_request: Internal::Util.base64_encode(csr.to_pem)
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def fetch_cert(csr)
|
107
|
+
uri = URI.parse @trusted_root.certificate_authority_for_signing.uri
|
108
|
+
uri = URI.join(uri, "api/v2/signingCert")
|
109
|
+
resp = Net::HTTP.post(
|
110
|
+
uri,
|
111
|
+
JSON.dump(csr),
|
112
|
+
{ "Content-Type" => "application/json" }
|
113
|
+
)
|
114
|
+
|
115
|
+
unless resp.code == "200"
|
116
|
+
raise Error::Signing,
|
117
|
+
"#{resp.code} #{resp.message}\n\n#{resp.body}"
|
118
|
+
end
|
119
|
+
|
120
|
+
resp_body = JSON.parse(resp.body)
|
121
|
+
|
122
|
+
unless resp_body.key?("signedCertificateEmbeddedSct")
|
123
|
+
raise Error::Signing, "missing signedCertificateEmbeddedSct in response from fulcio"
|
124
|
+
end
|
125
|
+
|
126
|
+
cert = resp_body.fetch("signedCertificateEmbeddedSct").fetch("chain")
|
127
|
+
.fetch("certificates").first.then { |pem| Internal::X509::Certificate.read(pem) }
|
128
|
+
logger.debug { "Fetched cert from fulcio" }
|
129
|
+
cert
|
130
|
+
end
|
131
|
+
|
132
|
+
def verify_chain(leaf)
|
133
|
+
# Perform certification path validation (RFC 5280 §6) of the returned certificate chain with the pre-distributed
|
134
|
+
# Fulcio root certificate(s) as a trust anchor.
|
135
|
+
|
136
|
+
x509_store = OpenSSL::X509::Store.new
|
137
|
+
expected_chain = @trusted_root.fulcio_cert_chain
|
138
|
+
|
139
|
+
x509_store.add_cert expected_chain.last.openssl
|
140
|
+
unless x509_store.verify(leaf.openssl, expected_chain[..-2].map(&:openssl))
|
141
|
+
raise Error::Signing, "returned certificate does not validate: #{x509_store.error_string}"
|
142
|
+
end
|
143
|
+
|
144
|
+
chain = x509_store.chain
|
145
|
+
chain.shift # remove the leaf cert
|
146
|
+
chain.map! { |cert| Internal::X509::Certificate.new(cert) }
|
147
|
+
|
148
|
+
logger.debug { "verified chain" }
|
149
|
+
|
150
|
+
# Extract a SignedCertificateTimestamp, which may be embedded as an X.509 extension in the leaf certificate or
|
151
|
+
# attached separately in the SigningCertificate returned from the Identity Service.
|
152
|
+
# Verify this SignedCertificateTimestamp as in RFC 9162 §8.1.3, using the root certificate from
|
153
|
+
# the Certificate Transparency Log.
|
154
|
+
if (result = @verifier.verify_scts(leaf, chain)) && !result.verified?
|
155
|
+
raise Error::Signing, "Failed to verify SCTs: #{result.reason}"
|
156
|
+
end
|
157
|
+
|
158
|
+
# Check that the leaf certificate contains the subject from the certificate signing request and encodes the
|
159
|
+
# appropriate AuthenticationServiceIdentifier in an extension with OID 1.3.6.1.4.1.57264.1.8.
|
160
|
+
|
161
|
+
fulcio_issuer = leaf.extension(Internal::X509::Extension::FulcioIssuer)
|
162
|
+
unless fulcio_issuer && fulcio_issuer.issuer == @identity_token.issuer
|
163
|
+
raise Error::Signing, "certificate does not contain expected Fulcio issuer"
|
164
|
+
end
|
165
|
+
|
166
|
+
unless leaf.subject.to_a.empty?
|
167
|
+
raise Error::Signing,
|
168
|
+
"certificate contains unexpected subject #{leaf.subject.to_a}"
|
169
|
+
end
|
170
|
+
|
171
|
+
general_names = leaf.extension(Internal::X509::Extension::SubjectAlternativeName).general_names
|
172
|
+
expected_san = [@identity_token.identity]
|
173
|
+
if general_names.map(&:last) != expected_san
|
174
|
+
raise Error::Signing,
|
175
|
+
"certificate does not contain expected SAN #{expected_san}, got #{general_names}"
|
176
|
+
end
|
177
|
+
|
178
|
+
[leaf, x509_store.chain]
|
179
|
+
end
|
180
|
+
|
181
|
+
def sign_payload(payload, key)
|
182
|
+
# The Signer MAY pre-hash the payload using a hash algorithm from the registry (Spec: Sigstore Registries) for
|
183
|
+
# compatibility with some signing metadata formats (see §Submission of Signing Metadata to Transparency Service).
|
184
|
+
key.sign("SHA256", payload)
|
185
|
+
end
|
186
|
+
|
187
|
+
# TODO: implement
|
188
|
+
def submit_signature_hash_to_timstamping_service(_signature)
|
189
|
+
# The Signer sends a hash of the signature as the messageImprint in a TimeStampReq to the Timestamping Service and
|
190
|
+
# receives a TimeStampResp including a `TimeStampToken`.
|
191
|
+
# The signer MUST verify the TimeStampToken against the payload and Timestamping Service root certificate.
|
192
|
+
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
|
196
|
+
def build_proposed_hashed_rekord_entry(signature, cert, hashed_input)
|
197
|
+
algorithm = case hashed_input.algorithm
|
198
|
+
when Common::V1::HashAlgorithm::SHA2_256 then "sha256"
|
199
|
+
when Common::V1::HashAlgorithm::SHA2_384 then "sha384"
|
200
|
+
when Common::V1::HashAlgorithm::SHA2_512 then "sha512"
|
201
|
+
else
|
202
|
+
raise ArgumentError,
|
203
|
+
"unsupported hash algorithm: #{hashed_input.algorithm.inspect}"
|
204
|
+
end
|
205
|
+
{
|
206
|
+
"spec" => {
|
207
|
+
"signature" => {
|
208
|
+
"content" => Internal::Util.base64_encode(signature),
|
209
|
+
"publicKey" => {
|
210
|
+
"content" => Internal::Util.base64_encode(cert.to_pem)
|
211
|
+
}
|
212
|
+
},
|
213
|
+
"data" => {
|
214
|
+
"hash" => {
|
215
|
+
"algorithm" => algorithm,
|
216
|
+
"value" => Internal::Util.hex_encode(hashed_input.digest)
|
217
|
+
}
|
218
|
+
}
|
219
|
+
},
|
220
|
+
"kind" => "hashedrekord",
|
221
|
+
"apiVersion" => "0.0.1"
|
222
|
+
}
|
223
|
+
end
|
224
|
+
|
225
|
+
def submit_signed_metadata_to_transparency_service(signature, cert, hashed_input)
|
226
|
+
# The Signer chooses a format for signing metadata; this format MUST be in the supportedMetadataFormats in the
|
227
|
+
# Transparency Service configuration. The Signer prepares signing metadata containing at a minimum:
|
228
|
+
# * The signature.
|
229
|
+
# * The payload (possibly pre-hashed; if so, the entry also includes the identifier of the hash algorithm).
|
230
|
+
# * Verification material (signing certificate or verification key).
|
231
|
+
# * If the verification material is a certificate, the client SHOULD upload only the signing certificate and
|
232
|
+
# SHOULD NOT upload the CA certificate chain.
|
233
|
+
#
|
234
|
+
# The signing metadata might contain additional, application-specific metadata according to the format used.
|
235
|
+
# The Signer then canonically encodes the metadata (according to the chosen format).
|
236
|
+
|
237
|
+
# TODO: allow configuring the entry kind?
|
238
|
+
proposed_entry = build_proposed_hashed_rekord_entry(signature, cert, hashed_input)
|
239
|
+
|
240
|
+
ctlog = @trusted_root.tlog_for_signing
|
241
|
+
logger.info { "Submitting to #{ctlog.base_url}" }
|
242
|
+
|
243
|
+
# The signer MUST verify the log entry as in Spec: Transparency Service.
|
244
|
+
@verifier.rekor_client.log.entries.post(proposed_entry)
|
245
|
+
end
|
246
|
+
|
247
|
+
def verify(artifact, bundle)
|
248
|
+
verification_input = Verification::V1::Input.new
|
249
|
+
verification_input.bundle = bundle
|
250
|
+
verification_input.artifact = Verification::V1::Artifact.new
|
251
|
+
verification_input.artifact.artifact = artifact
|
252
|
+
|
253
|
+
result = @verifier.verify(
|
254
|
+
input: VerificationInput.new(verification_input),
|
255
|
+
policy: expected_identity,
|
256
|
+
offline: false
|
257
|
+
)
|
258
|
+
raise Error::Signing, "Failed to verify: #{result.reason}" unless result.verified?
|
259
|
+
end
|
260
|
+
|
261
|
+
def expected_identity
|
262
|
+
Policy::Identity.new(identity: @identity_token.identity, issuer: @identity_token.issuer)
|
263
|
+
end
|
264
|
+
|
265
|
+
def collect_bundle(leaf_certificate, tlog_entries, timestamp_verification_data, hashed_input, signature)
|
266
|
+
bundle = Bundle::V1::Bundle.new
|
267
|
+
bundle.media_type = BundleType::BUNDLE_0_3.media_type
|
268
|
+
bundle.verification_material = Bundle::V1::VerificationMaterial.new
|
269
|
+
bundle.verification_material.certificate = Common::V1::X509Certificate.new
|
270
|
+
bundle.verification_material.certificate.raw_bytes = leaf_certificate.to_pem
|
271
|
+
bundle.verification_material.tlog_entries = tlog_entries
|
272
|
+
bundle.verification_material.timestamp_verification_data = timestamp_verification_data
|
273
|
+
bundle.message_signature = Sigstore::Common::V1::MessageSignature.new.tap do |ms|
|
274
|
+
ms.message_digest = hashed_input
|
275
|
+
ms.signature = signature
|
276
|
+
end
|
277
|
+
bundle
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,116 @@
|
|
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 "delegate"
|
18
|
+
require "openssl"
|
19
|
+
|
20
|
+
require "protobug_sigstore_protos"
|
21
|
+
|
22
|
+
require_relative "tuf"
|
23
|
+
|
24
|
+
module Sigstore
|
25
|
+
REGISTRY = Protobug::Registry.new do |registry|
|
26
|
+
Sigstore::TrustRoot::V1.register_sigstore_trustroot_protos(registry)
|
27
|
+
Sigstore::Bundle::V1.register_sigstore_bundle_protos(registry)
|
28
|
+
end
|
29
|
+
class TrustedRoot < DelegateClass(Sigstore::TrustRoot::V1::TrustedRoot)
|
30
|
+
def self.production(offline: false)
|
31
|
+
from_tuf(TUF::DEFAULT_TUF_URL, offline)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.staging(offline: false)
|
35
|
+
from_tuf(TUF::STAGING_TUF_URL, offline)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_tuf(url, offline)
|
39
|
+
path = TUF::TrustUpdater.new(url, offline).tap { _1.refresh unless offline }.trusted_root_path
|
40
|
+
from_file(path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.from_file(path)
|
44
|
+
contents = Gem.read_binary(path)
|
45
|
+
new Sigstore::TrustRoot::V1::TrustedRoot.decode_json(contents, registry: REGISTRY)
|
46
|
+
end
|
47
|
+
|
48
|
+
def rekor_keys
|
49
|
+
keys = tlog_keys(tlogs).to_a
|
50
|
+
raise Error::InvalidBundle, "Did not find one Rekor key" if keys.size != 1
|
51
|
+
|
52
|
+
keys
|
53
|
+
end
|
54
|
+
|
55
|
+
def ctfe_keys
|
56
|
+
keys = tlog_keys(ctlogs).to_a
|
57
|
+
raise Error::InvalidBundle, "Did not find any CTFE keys" if keys.empty?
|
58
|
+
|
59
|
+
keys
|
60
|
+
end
|
61
|
+
|
62
|
+
def fulcio_cert_chain
|
63
|
+
certs = ca_keys(certificate_authorities, allow_expired: true).flat_map do |raw_bytes|
|
64
|
+
Internal::X509::Certificate.read(raw_bytes)
|
65
|
+
end
|
66
|
+
raise Error::InvalidBundle, "Fulcio certificates not found in trusted root" if certs.empty?
|
67
|
+
|
68
|
+
certs
|
69
|
+
end
|
70
|
+
|
71
|
+
def tlog_for_signing
|
72
|
+
tlogs.find do |ctlog|
|
73
|
+
timerange_valid?(ctlog.public_key.valid_for, allow_expired: false)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def certificate_authority_for_signing
|
78
|
+
certificate_authorities.find do |ca|
|
79
|
+
timerange_valid?(ca.valid_for, allow_expired: false)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def tlog_keys(tlogs)
|
86
|
+
return enum_for(__method__, tlogs) unless block_given?
|
87
|
+
|
88
|
+
tlogs.each do |transparency_log_instance|
|
89
|
+
key = transparency_log_instance.public_key
|
90
|
+
yield Internal::Key.from_key_details(key.key_details, key.raw_bytes)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def ca_keys(certificate_authorities, allow_expired:)
|
95
|
+
return enum_for(__method__, certificate_authorities, allow_expired:) unless block_given?
|
96
|
+
|
97
|
+
certificate_authorities.each do |ca|
|
98
|
+
next unless timerange_valid?(ca.valid_for, allow_expired:)
|
99
|
+
|
100
|
+
ca.cert_chain.certificates.each do |cert|
|
101
|
+
yield cert.raw_bytes
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def timerange_valid?(period, allow_expired:)
|
107
|
+
now = Time.now.utc
|
108
|
+
return true unless period
|
109
|
+
return false if now < period.start.to_time
|
110
|
+
return true if allow_expired
|
111
|
+
return false if period.end && now > period.end.to_time
|
112
|
+
|
113
|
+
true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
+
module TUF
|
19
|
+
class UpdaterConfig
|
20
|
+
attr_reader :max_root_rotations, :max_delegations, :root_max_length, :timestamp_max_length, :snapshot_max_length,
|
21
|
+
:targets_max_length, :prefix_targets_with_hash, :envelope_type, :app_user_agent
|
22
|
+
|
23
|
+
def initialize(
|
24
|
+
max_root_rotations: 32,
|
25
|
+
max_delegations: 32,
|
26
|
+
root_max_length: 512_000, # bytes
|
27
|
+
timestamp_max_length: 16_384, # bytes
|
28
|
+
snapshot_max_length: 2_000_000, # bytes
|
29
|
+
targets_max_length: 5_000_000, # bytes
|
30
|
+
prefix_targets_with_hash: true,
|
31
|
+
envelope_type: :metadata,
|
32
|
+
app_user_agent: nil
|
33
|
+
)
|
34
|
+
@max_root_rotations = max_root_rotations
|
35
|
+
@max_delegations = max_delegations
|
36
|
+
@root_max_length = root_max_length
|
37
|
+
@timestamp_max_length = timestamp_max_length
|
38
|
+
@snapshot_max_length = snapshot_max_length
|
39
|
+
@targets_max_length = targets_max_length
|
40
|
+
@prefix_targets_with_hash = prefix_targets_with_hash
|
41
|
+
@envelope_type = envelope_type
|
42
|
+
@app_user_agent = app_user_agent
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,49 @@
|
|
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 "../error"
|
18
|
+
|
19
|
+
module Sigstore::TUF
|
20
|
+
class Error < ::Sigstore::Error
|
21
|
+
# An error with a repository's state, such as a missing file.
|
22
|
+
class RepositoryError < Error; end
|
23
|
+
|
24
|
+
class LengthOrHashMismatch < RepositoryError; end
|
25
|
+
class ExpiredMetadata < RepositoryError; end
|
26
|
+
class BadVersionNumber < RepositoryError; end
|
27
|
+
class EqualVersionNumber < BadVersionNumber; end
|
28
|
+
class TooFewSignatures < RepositoryError; end
|
29
|
+
|
30
|
+
class BadUpdateOrder < Error; end
|
31
|
+
class InvalidData < Error; end
|
32
|
+
class DuplicateKeys < Error; end
|
33
|
+
|
34
|
+
# An error occurred while attempting to download a file.
|
35
|
+
class DownloadError < Error; end
|
36
|
+
|
37
|
+
class Fetch < Error; end
|
38
|
+
class RemoteConnection < Fetch; end
|
39
|
+
|
40
|
+
class UnsuccessfulResponse < Fetch
|
41
|
+
attr_reader :response
|
42
|
+
|
43
|
+
def initialize(message, response)
|
44
|
+
super(message)
|
45
|
+
@response = response
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|