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,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
|