36
36
X509StoreFlags ,
37
37
)
38
38
from pydantic import ValidationError
39
+ from rfc3161_client import TimeStampResponse , VerifierBuilder
40
+ from rfc3161_client import VerificationError as Rfc3161VerificationError
39
41
40
42
from sigstore import dsse
41
43
from sigstore ._internal .rekor import _hashedrekord_from_parts
44
46
_get_precertificate_signed_certificate_timestamps ,
45
47
verify_sct ,
46
48
)
49
+ from sigstore ._internal .timestamp import TimestampSource , TimestampVerificationResult
47
50
from sigstore ._internal .trust import ClientTrustConfig , KeyringPurpose , TrustedRoot
48
51
from sigstore ._utils import base64_encode_pem_cert , sha256_digest
49
52
from sigstore .errors import VerificationError
53
56
54
57
_logger = logging .getLogger (__name__ )
55
58
59
+ # Limit the number of timestamps to prevent DoS
60
+ # From https://github.com/sigstore/sigstore-go/blob/e92142f0734064ebf6001f188b7330a1212245fe/pkg/verify/tsa.go#L29
61
+ MAX_ALLOWED_TIMESTAMP : int = 32
62
+
63
+ # When verifying a timestamp, this threshold represents the minimum number of required
64
+ # timestamps to consider a signature valid.
65
+ VERIFY_TIMESTAMP_THRESHOLD : int = 1
66
+
56
67
57
68
class Verifier :
58
69
"""
@@ -108,6 +119,155 @@ def _from_trust_config(cls, trust_config: ClientTrustConfig) -> Verifier:
108
119
trusted_root = trust_config .trusted_root ,
109
120
)
110
121
122
+ def _verify_signed_timestamp (
123
+ self , timestamp_response : TimeStampResponse , signature : bytes
124
+ ) -> TimestampVerificationResult | None :
125
+ """
126
+ Verify a Signed Timestamp using the TSA provided by the Trusted Root.
127
+ """
128
+ cert_authorities = self ._trusted_root .get_timestamp_authorities ()
129
+ for certificate_authority in cert_authorities :
130
+ certificates = certificate_authority .certificates (allow_expired = True )
131
+
132
+ builder = VerifierBuilder ()
133
+ for certificate in certificates :
134
+ builder .add_root_certificate (certificate )
135
+
136
+ verifier = builder .build ()
137
+ try :
138
+ verifier .verify (timestamp_response , signature )
139
+ except Rfc3161VerificationError as e :
140
+ _logger .debug ("Unable to verify Timestamp with CA." )
141
+ _logger .exception (e )
142
+ continue
143
+
144
+ if (
145
+ certificate_authority .validity_period_start
146
+ and certificate_authority .validity_period_end
147
+ ):
148
+ if (
149
+ certificate_authority .validity_period_start
150
+ <= timestamp_response .tst_info .gen_time
151
+ < certificate_authority .validity_period_end
152
+ ):
153
+ return TimestampVerificationResult (
154
+ source = TimestampSource .TIMESTAMP_AUTHORITY ,
155
+ time = timestamp_response .tst_info .gen_time ,
156
+ )
157
+
158
+ _logger .debug (
159
+ "Unable to verify Timestamp because not in CA time range."
160
+ )
161
+ else :
162
+ _logger .debug (
163
+ "Unable to verify Timestamp because no validity provided."
164
+ )
165
+
166
+ return None
167
+
168
+ def _verify_timestamp_authority (
169
+ self , bundle : Bundle
170
+ ) -> List [TimestampVerificationResult ]:
171
+ """
172
+ Verify that the given bundle has been timestamped by a trusted timestamp authority
173
+ and that the timestamp is valid.
174
+
175
+ Returns the number of valid signed timestamp in the bundle.
176
+ """
177
+ timestamp_responses = (
178
+ bundle .verification_material .timestamp_verification_data .rfc3161_timestamps
179
+ )
180
+ if len (timestamp_responses ) > MAX_ALLOWED_TIMESTAMP :
181
+ msg = f"Too many signed timestamp: { len (timestamp_responses )} > { MAX_ALLOWED_TIMESTAMP } "
182
+ raise VerificationError (msg )
183
+
184
+ if len (set (timestamp_responses )) != len (timestamp_responses ):
185
+ msg = "Duplicate timestamp found"
186
+ raise VerificationError (msg )
187
+
188
+ # The Signer sends a hash of the signature as the messageImprint in a TimeStampReq
189
+ # to the Timestamping Service
190
+ signature_hash = sha256_digest (bundle .signature ).digest
191
+ verified_timestamps = []
192
+ for tsr in timestamp_responses :
193
+ if verified_timestamp := self ._verify_signed_timestamp (tsr , signature_hash ):
194
+ verified_timestamps .append (verified_timestamp )
195
+
196
+ return verified_timestamps
197
+
198
+ def _establish_time (self , bundle : Bundle ) -> List [TimestampVerificationResult ]:
199
+ """
200
+ Establish the time for bundle verification.
201
+
202
+ This method uses timestamps from two possible sources:
203
+ 1. RFC3161 signed timestamps from a Timestamping Authority (TSA)
204
+ 2. Transparency Log timestamps
205
+ """
206
+ verified_timestamps = []
207
+
208
+ # If a timestamp from the timestamping service is available, the Verifier MUST
209
+ # perform path validation using the timestamp from the Timestamping Service.
210
+ if bundle .verification_material .timestamp_verification_data .rfc3161_timestamps :
211
+ if not self ._trusted_root .get_timestamp_authorities ():
212
+ msg = (
213
+ "no Timestamp Authorities have been provided to validate this "
214
+ "bundle but it contains a signed timestamp"
215
+ )
216
+ raise VerificationError (msg )
217
+
218
+ timestamp_from_tsa = self ._verify_timestamp_authority (bundle )
219
+ if len (timestamp_from_tsa ) < VERIFY_TIMESTAMP_THRESHOLD :
220
+ msg = (
221
+ f"not enough timestamps validated to meet the validation "
222
+ f"threshold ({ len (timestamp_from_tsa )} /{ VERIFY_TIMESTAMP_THRESHOLD } )"
223
+ )
224
+ raise VerificationError (msg )
225
+
226
+ verified_timestamps .extend (timestamp_from_tsa )
227
+
228
+ # If a timestamp from the Transparency Service is available, the Verifier MUST
229
+ # perform path validation using the timestamp from the Transparency Service.
230
+ if timestamp := bundle .log_entry .integrated_time :
231
+ verified_timestamps .append (
232
+ TimestampVerificationResult (
233
+ source = TimestampSource .TRANSPARENCY_SERVICE ,
234
+ time = datetime .fromtimestamp (timestamp , tz = timezone .utc ),
235
+ )
236
+ )
237
+ return verified_timestamps
238
+
239
+ def _verify_chain_at_time (
240
+ self , certificate : X509 , timestamp_result : TimestampVerificationResult
241
+ ) -> List [X509 ]:
242
+ """
243
+ Verify the validity of the certificate chain at the given time.
244
+
245
+ Raises a VerificationError if the chain can't be built or be verified.
246
+ """
247
+ # NOTE: The `X509Store` object cannot have its time reset once the `set_time`
248
+ # method been called on it. To get around this, we construct a new one in each
249
+ # call.
250
+ store = X509Store ()
251
+ # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's
252
+ # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN
253
+ # would be strictly more conformant of OpenSSL, but we currently
254
+ # *want* the "long" chain behavior of performing path validation
255
+ # down to a self-signed root.
256
+ store .set_flags (X509StoreFlags .X509_STRICT )
257
+ for parent_cert_ossl in self ._fulcio_certificate_chain :
258
+ store .add_cert (parent_cert_ossl )
259
+
260
+ store .set_time (timestamp_result .time )
261
+
262
+ store_ctx = X509StoreContext (store , certificate )
263
+
264
+ try :
265
+ # get_verified_chain returns the full chain including the end-entity certificate
266
+ # and chain should contain only CA certificates
267
+ return store_ctx .get_verified_chain ()[1 :]
268
+ except X509StoreContextError as e :
269
+ raise VerificationError (f"failed to build chain: { e } " )
270
+
111
271
def _verify_common_signing_cert (
112
272
self , bundle : Bundle , policy : VerificationPolicy
113
273
) -> None :
@@ -120,6 +280,7 @@ def _verify_common_signing_cert(
120
280
121
281
# In order to verify an artifact, we need to achieve the following:
122
282
#
283
+ # 0. Establish a time for the signature.
123
284
# 1. Verify that the signing certificate chains to the root of trust
124
285
# and is valid at the time of signing.
125
286
# 2. Verify the signing certificate's SCT.
@@ -135,7 +296,7 @@ def _verify_common_signing_cert(
135
296
# 8. Verify the transparency log entry's consistency against the other
136
297
# materials, to prevent variants of CVE-2022-36056.
137
298
#
138
- # This method performs steps (1 ) through (6) above. Its caller
299
+ # This method performs steps (0 ) through (6) above. Its caller
139
300
# MUST perform steps (7) and (8) separately, since they vary based on
140
301
# the kind of verification being performed (i.e. hashedrekord, DSSE, etc.)
141
302
@@ -154,20 +315,23 @@ def _verify_common_signing_cert(
154
315
for parent_cert_ossl in self ._fulcio_certificate_chain :
155
316
store .add_cert (parent_cert_ossl )
156
317
318
+ # (0): Establishing a Time for the Signature
319
+ # First, establish a time for the signature. This timestamp is required to
320
+ # validate the certificate chain, so this step comes first.
321
+ # While this step is optional and only performed if timestamp data has been
322
+ # provided within the bundle, providing a signed timestamp without a TSA to
323
+ # verify it result in a VerificationError.
324
+ verified_timestamps = self ._establish_time (bundle )
325
+ if not verified_timestamps :
326
+ raise VerificationError ("not enough sources of verified time" )
327
+
157
328
# (1): verify that the signing certificate is signed by the root
158
329
# certificate and that the signing certificate was valid at the
159
330
# time of signing.
160
- sign_date = cert .not_valid_before_utc
161
331
cert_ossl = X509 .from_cryptography (cert )
162
-
163
- store .set_time (sign_date )
164
- store_ctx = X509StoreContext (store , cert_ossl )
165
- try :
166
- # get_verified_chain returns the full chain including the end-entity certificate
167
- # and chain should contain only CA certificates
168
- chain = store_ctx .get_verified_chain ()[1 :]
169
- except X509StoreContextError as e :
170
- raise VerificationError (f"failed to build chain: { e } " )
332
+ chain : list [X509 ] = []
333
+ for vts in verified_timestamps :
334
+ chain = self ._verify_chain_at_time (cert_ossl , vts )
171
335
172
336
# (2): verify the signing certificate's SCT.
173
337
sct = _get_precertificate_signed_certificate_timestamps (cert )[0 ]
0 commit comments