Skip to content

Commit 6633b87

Browse files
committed
Use verified time in verification
Signed-off-by: Alexis <[email protected]>
1 parent 5f23327 commit 6633b87

File tree

3 files changed

+152
-41
lines changed

3 files changed

+152
-41
lines changed

Diff for: sigstore/timestamp.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2022 The Sigstore Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Utilities to deal with Signed Timestamps.
17+
"""
18+
19+
import enum
20+
from dataclasses import dataclass
21+
from datetime import datetime
22+
23+
24+
class TimestampSource(enum.Enum):
25+
"""Represents the source of a timestamp."""
26+
27+
TIMESTAMP_AUTHORITY = enum.auto()
28+
TRANSPARENCY_SERVICE = enum.auto()
29+
30+
31+
@dataclass
32+
class TimestampVerificationResult:
33+
"""Represents a timestamp used by the Verifier.
34+
35+
A Timestamp either comes from a Timestamping Service (RFC3161) or the Transparency
36+
Service.
37+
"""
38+
39+
source: TimestampSource
40+
time: datetime

Diff for: sigstore/verify/verifier.py

+96-36
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import base64
2222
import logging
2323
from datetime import datetime, timezone
24-
from typing import List, cast
24+
from typing import List, Union, cast
2525

2626
import rekor_types
2727
from cryptography.exceptions import InvalidSignature
@@ -51,6 +51,7 @@
5151
from sigstore.errors import VerificationError
5252
from sigstore.hashes import Hashed
5353
from sigstore.models import Bundle
54+
from sigstore.timestamp import TimestampSource, TimestampVerificationResult
5455
from sigstore.verify.policy import VerificationPolicy
5556

5657
_logger = logging.getLogger(__name__)
@@ -120,7 +121,7 @@ def _from_trust_config(cls, trust_config: ClientTrustConfig) -> Verifier:
120121

121122
def _verify_signed_timestamp(
122123
self, timestamp_response: TimeStampResponse, signature: bytes
123-
) -> bool:
124+
) -> Union[None, TimestampVerificationResult]:
124125
"""
125126
Verify a Signed Timestamp using the TSA provided by the Trusted Root.
126127
"""
@@ -150,7 +151,10 @@ def _verify_signed_timestamp(
150151
<= timestamp_response.tst_info.gen_time
151152
< certificate_authority.validity_period_end
152153
):
153-
return True
154+
return TimestampVerificationResult(
155+
source=TimestampSource.TIMESTAMP_AUTHORITY,
156+
time=timestamp_response.tst_info.gen_time,
157+
)
154158

155159
_logger.debug(
156160
"Unable to verify Timestamp because not in CA time range."
@@ -160,9 +164,11 @@ def _verify_signed_timestamp(
160164
"Unable to verify Timestamp because no validity provided."
161165
)
162166

163-
return False
167+
return None
164168

165-
def _verify_timestamp_authority(self, bundle: Bundle) -> int:
169+
def _verify_timestamp_authority(
170+
self, bundle: Bundle
171+
) -> List[TimestampVerificationResult]:
166172
"""
167173
Verify that the given bundle has been timestamped by a trusted timestamp authority
168174
and that the timestamp is valid.
@@ -183,10 +189,85 @@ def _verify_timestamp_authority(self, bundle: Bundle) -> int:
183189
# The Signer sends a hash of the signature as the messageImprint in a TimeStampReq
184190
# to the Timestamping Service
185191
signature_hash = sha256_digest(bundle.signature).digest
186-
return [
187-
self._verify_signed_timestamp(tsr, signature_hash)
188-
for tsr in timestamp_responses
189-
].count(True)
192+
verified_timestamps: List[TimestampVerificationResult] = []
193+
for tsr in timestamp_responses:
194+
if verified_timestamp := self._verify_signed_timestamp(tsr, signature_hash):
195+
verified_timestamps.append(verified_timestamp)
196+
197+
return verified_timestamps
198+
199+
def _establish_time(self, bundle: Bundle) -> List[TimestampVerificationResult]:
200+
"""
201+
Establish timestamps source for the verification.
202+
203+
We both source signed timestamp (per RFC3161) and Transparency Log timestamp as
204+
time sources. As per the spec, if both are available, the Verifier performs
205+
path validation twice. If either fails, verification fails.
206+
"""
207+
verified_timestamps: List[TimestampVerificationResult] = []
208+
209+
# If a timestamp from the timestamping service is available, the Verifier MUST
210+
# perform path validation using the timestamp from the Timestamping Service.
211+
if bundle.verification_material.timestamp_verification_data.rfc3161_timestamps:
212+
if not self._trusted_root.get_timestamp_authorities():
213+
msg = (
214+
"no Timestamp Authorities have been provided to validate this "
215+
"bundle but it contains a signed timestamp"
216+
)
217+
raise VerificationError(msg)
218+
219+
timestamp_from_tsa = self._verify_timestamp_authority(bundle)
220+
if len(timestamp_from_tsa) < VERIFY_TIMESTAMP_THRESHOLD:
221+
msg = (
222+
f"not enough timestamps validated to meet the validation "
223+
f"threshold ({len(timestamp_from_tsa)}/{VERIFY_TIMESTAMP_THRESHOLD})"
224+
)
225+
raise VerificationError(msg)
226+
227+
verified_timestamps.extend(timestamp_from_tsa)
228+
229+
# If a timestamp from the Transparency Service is available, the Verifier MUST
230+
# perform path validation using the timestamp from the Transparency Service.
231+
if timestamp := bundle.log_entry.integrated_time:
232+
verified_timestamps.append(
233+
TimestampVerificationResult(
234+
source=TimestampSource.TRANSPARENCY_SERVICE,
235+
time=datetime.fromtimestamp(timestamp, tz=timezone.utc),
236+
)
237+
)
238+
return verified_timestamps
239+
240+
def _verify_chain_at_time(
241+
self, certificate: X509, timestamp_result: TimestampVerificationResult
242+
) -> List[X509]:
243+
"""
244+
Verify the validity of the certificate chain at the given tive.
245+
246+
Raises a VerificationError if the chain can't be built or be verified.
247+
"""
248+
# NOTE: The `X509Store` object cannot have its time reset once the `set_time`
249+
# method been called on it. To get around this, we construct a new one in each
250+
# call.
251+
store = X509Store()
252+
# NOTE: By explicitly setting the flags here, we ensure that OpenSSL's
253+
# PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN
254+
# would be strictly more conformant of OpenSSL, but we currently
255+
# *want* the "long" chain behavior of performing path validation
256+
# down to a self-signed root.
257+
store.set_flags(X509StoreFlags.X509_STRICT)
258+
for parent_cert_ossl in self._fulcio_certificate_chain:
259+
store.add_cert(parent_cert_ossl)
260+
261+
store.set_time(timestamp_result.time)
262+
263+
store_ctx = X509StoreContext(store, certificate)
264+
265+
try:
266+
# get_verified_chain returns the full chain including the end-entity certificate
267+
# and chain should contain only CA certificates
268+
return store_ctx.get_verified_chain()[1:]
269+
except X509StoreContextError as e:
270+
raise VerificationError(f"failed to build chain: {e}")
190271

191272
def _verify_common_signing_cert(
192273
self, bundle: Bundle, policy: VerificationPolicy
@@ -240,38 +321,17 @@ def _verify_common_signing_cert(
240321
# While this step is optional and only performed if timestamp data has been
241322
# provided within the bundle, providing a signed timestamp without a TSA to
242323
# verify it result in a VerificationError.
243-
if bundle.verification_material.timestamp_verification_data.rfc3161_timestamps:
244-
if not self._trusted_root.get_timestamp_authorities():
245-
msg = (
246-
"No Timestamp Authorities have been provided to validate this "
247-
"bundle but it contains a signed timestamp"
248-
)
249-
raise VerificationError(msg)
250-
251-
verified_timestamp = self._verify_timestamp_authority(bundle)
252-
# The threshold is set to (1) by default but kept as a variable to allow
253-
# this value to change
254-
if verified_timestamp < VERIFY_TIMESTAMP_THRESHOLD:
255-
msg = (
256-
f"Not enough Timestamp validated to meet the Validation "
257-
f"Threshold ({verified_timestamp}/{VERIFY_TIMESTAMP_THRESHOLD})"
258-
)
259-
raise VerificationError(msg)
324+
verified_timestamps = self._establish_time(bundle)
325+
if not verified_timestamps:
326+
raise VerificationError("not enough sources of verified time")
260327

261328
# (1): verify that the signing certificate is signed by the root
262329
# certificate and that the signing certificate was valid at the
263330
# time of signing.
264-
sign_date = cert.not_valid_before_utc
265331
cert_ossl = X509.from_cryptography(cert)
266-
267-
store.set_time(sign_date)
268-
store_ctx = X509StoreContext(store, cert_ossl)
269-
try:
270-
# get_verified_chain returns the full chain including the end-entity certificate
271-
# and chain should contain only CA certificates
272-
chain = store_ctx.get_verified_chain()[1:]
273-
except X509StoreContextError as e:
274-
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)
275335

276336
# (2): verify the signing certificate's SCT.
277337
sct = _get_precertificate_signed_certificate_timestamps(cert)[0]

Diff for: test/unit/verify/test_verifier.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ def test_verifier_verify_timestamp(self, verifier, asset, null_policy):
212212
null_policy,
213213
)
214214

215+
def test_verifier_without_timestamp(
216+
self, verifier, asset, null_policy, monkeypatch
217+
):
218+
monkeypatch.setattr(verifier, "_establish_time", lambda *args: [])
219+
with pytest.raises(VerificationError, match="not enough sources"):
220+
verifier.verify_artifact(
221+
asset("tsa/bundle.txt").read_bytes(),
222+
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
223+
null_policy,
224+
)
225+
215226
def test_verifier_too_many_timestamp(self, verifier, asset, null_policy):
216227
with pytest.raises(VerificationError, match="Too many"):
217228
verifier.verify_artifact(
@@ -236,7 +247,7 @@ def test_verifier_no_validity(self, caplog, verifier, asset, null_policy):
236247
]._inner.valid_for.end = None
237248

238249
with caplog.at_level(logging.DEBUG, logger="sigstore.verify.verifier"):
239-
with pytest.raises(VerificationError, match="Not enough Timestamp"):
250+
with pytest.raises(VerificationError, match="not enough timestamps"):
240251
verifier.verify_artifact(
241252
asset("tsa/bundle.txt").read_bytes(),
242253
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
@@ -257,7 +268,7 @@ def test_verifier_outside_validity_range(
257268
]._inner.valid_for.end = datetime(2024, 10, 31, tzinfo=timezone.utc)
258269

259270
with caplog.at_level(logging.DEBUG, logger="sigstore.verify.verifier"):
260-
with pytest.raises(VerificationError, match="Not enough Timestamp"):
271+
with pytest.raises(VerificationError, match="not enough timestamps"):
261272
verifier.verify_artifact(
262273
asset("tsa/bundle.txt").read_bytes(),
263274
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
@@ -278,7 +289,7 @@ def verify_function(*args):
278289
monkeypatch.setattr(rfc3161_client.verify._Verifier, "verify", verify_function)
279290

280291
with caplog.at_level(logging.DEBUG, logger="sigstore.verify.verifier"):
281-
with pytest.raises(VerificationError, match="Not enough Timestamp"):
292+
with pytest.raises(VerificationError, match="not enough timestamps"):
282293
verifier.verify_artifact(
283294
asset("tsa/bundle.txt").read_bytes(),
284295
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
@@ -291,7 +302,7 @@ def test_verifier_no_authorities(self, asset, null_policy):
291302
verifier = Verifier.staging(offline=True)
292303
verifier._trusted_root._inner.timestamp_authorities = []
293304

294-
with pytest.raises(VerificationError, match="No Timestamp Authorities"):
305+
with pytest.raises(VerificationError, match="no Timestamp Authorities"):
295306
verifier.verify_artifact(
296307
asset("tsa/bundle.txt").read_bytes(),
297308
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
@@ -302,7 +313,7 @@ def test_verifier_not_enough_timestamp(
302313
self, verifier, asset, null_policy, monkeypatch
303314
):
304315
monkeypatch.setattr("sigstore.verify.verifier.VERIFY_TIMESTAMP_THRESHOLD", 2)
305-
with pytest.raises(VerificationError, match="Not enough Timestamp"):
316+
with pytest.raises(VerificationError, match="not enough timestamps"):
306317
verifier.verify_artifact(
307318
asset("tsa/bundle.txt").read_bytes(),
308319
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),

0 commit comments

Comments
 (0)