Skip to content

Commit

Permalink
Use verified time in verification
Browse files Browse the repository at this point in the history
Signed-off-by: Alexis <[email protected]>
  • Loading branch information
DarkaMaul committed Nov 13, 2024
1 parent 5f23327 commit 6633b87
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 41 deletions.
40 changes: 40 additions & 0 deletions sigstore/timestamp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2022 The Sigstore Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Utilities to deal with Signed Timestamps.
"""

import enum
from dataclasses import dataclass
from datetime import datetime


class TimestampSource(enum.Enum):
"""Represents the source of a timestamp."""

TIMESTAMP_AUTHORITY = enum.auto()
TRANSPARENCY_SERVICE = enum.auto()


@dataclass
class TimestampVerificationResult:
"""Represents a timestamp used by the Verifier.
A Timestamp either comes from a Timestamping Service (RFC3161) or the Transparency
Service.
"""

source: TimestampSource
time: datetime
132 changes: 96 additions & 36 deletions sigstore/verify/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import base64
import logging
from datetime import datetime, timezone
from typing import List, cast
from typing import List, Union, cast

import rekor_types
from cryptography.exceptions import InvalidSignature
Expand Down Expand Up @@ -51,6 +51,7 @@
from sigstore.errors import VerificationError
from sigstore.hashes import Hashed
from sigstore.models import Bundle
from sigstore.timestamp import TimestampSource, TimestampVerificationResult
from sigstore.verify.policy import VerificationPolicy

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

def _verify_signed_timestamp(
self, timestamp_response: TimeStampResponse, signature: bytes
) -> bool:
) -> Union[None, TimestampVerificationResult]:
"""
Verify a Signed Timestamp using the TSA provided by the Trusted Root.
"""
Expand Down Expand Up @@ -150,7 +151,10 @@ def _verify_signed_timestamp(
<= timestamp_response.tst_info.gen_time
< certificate_authority.validity_period_end
):
return True
return TimestampVerificationResult(
source=TimestampSource.TIMESTAMP_AUTHORITY,
time=timestamp_response.tst_info.gen_time,
)

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

return False
return None

def _verify_timestamp_authority(self, bundle: Bundle) -> int:
def _verify_timestamp_authority(
self, bundle: Bundle
) -> List[TimestampVerificationResult]:
"""
Verify that the given bundle has been timestamped by a trusted timestamp authority
and that the timestamp is valid.
Expand All @@ -183,10 +189,85 @@ def _verify_timestamp_authority(self, bundle: Bundle) -> int:
# The Signer sends a hash of the signature as the messageImprint in a TimeStampReq
# to the Timestamping Service
signature_hash = sha256_digest(bundle.signature).digest
return [
self._verify_signed_timestamp(tsr, signature_hash)
for tsr in timestamp_responses
].count(True)
verified_timestamps: List[TimestampVerificationResult] = []
for tsr in timestamp_responses:
if verified_timestamp := self._verify_signed_timestamp(tsr, signature_hash):
verified_timestamps.append(verified_timestamp)

return verified_timestamps

def _establish_time(self, bundle: Bundle) -> List[TimestampVerificationResult]:
"""
Establish timestamps source for the verification.
We both source signed timestamp (per RFC3161) and Transparency Log timestamp as
time sources. As per the spec, if both are available, the Verifier performs
path validation twice. If either fails, verification fails.
"""
verified_timestamps: List[TimestampVerificationResult] = []

# If a timestamp from the timestamping service is available, the Verifier MUST
# perform path validation using the timestamp from the Timestamping Service.
if bundle.verification_material.timestamp_verification_data.rfc3161_timestamps:
if not self._trusted_root.get_timestamp_authorities():
msg = (
"no Timestamp Authorities have been provided to validate this "
"bundle but it contains a signed timestamp"
)
raise VerificationError(msg)

timestamp_from_tsa = self._verify_timestamp_authority(bundle)
if len(timestamp_from_tsa) < VERIFY_TIMESTAMP_THRESHOLD:
msg = (
f"not enough timestamps validated to meet the validation "
f"threshold ({len(timestamp_from_tsa)}/{VERIFY_TIMESTAMP_THRESHOLD})"
)
raise VerificationError(msg)

verified_timestamps.extend(timestamp_from_tsa)

# If a timestamp from the Transparency Service is available, the Verifier MUST
# perform path validation using the timestamp from the Transparency Service.
if timestamp := bundle.log_entry.integrated_time:
verified_timestamps.append(
TimestampVerificationResult(
source=TimestampSource.TRANSPARENCY_SERVICE,
time=datetime.fromtimestamp(timestamp, tz=timezone.utc),
)
)
return verified_timestamps

def _verify_chain_at_time(
self, certificate: X509, timestamp_result: TimestampVerificationResult
) -> List[X509]:
"""
Verify the validity of the certificate chain at the given tive.
Raises a VerificationError if the chain can't be built or be verified.
"""
# NOTE: The `X509Store` object cannot have its time reset once the `set_time`
# method been called on it. To get around this, we construct a new one in each
# call.
store = X509Store()
# NOTE: By explicitly setting the flags here, we ensure that OpenSSL's
# PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN
# would be strictly more conformant of OpenSSL, but we currently
# *want* the "long" chain behavior of performing path validation
# down to a self-signed root.
store.set_flags(X509StoreFlags.X509_STRICT)
for parent_cert_ossl in self._fulcio_certificate_chain:
store.add_cert(parent_cert_ossl)

store.set_time(timestamp_result.time)

store_ctx = X509StoreContext(store, certificate)

try:
# get_verified_chain returns the full chain including the end-entity certificate
# and chain should contain only CA certificates
return store_ctx.get_verified_chain()[1:]
except X509StoreContextError as e:
raise VerificationError(f"failed to build chain: {e}")

def _verify_common_signing_cert(
self, bundle: Bundle, policy: VerificationPolicy
Expand Down Expand Up @@ -240,38 +321,17 @@ def _verify_common_signing_cert(
# While this step is optional and only performed if timestamp data has been
# provided within the bundle, providing a signed timestamp without a TSA to
# verify it result in a VerificationError.
if bundle.verification_material.timestamp_verification_data.rfc3161_timestamps:
if not self._trusted_root.get_timestamp_authorities():
msg = (
"No Timestamp Authorities have been provided to validate this "
"bundle but it contains a signed timestamp"
)
raise VerificationError(msg)

verified_timestamp = self._verify_timestamp_authority(bundle)
# The threshold is set to (1) by default but kept as a variable to allow
# this value to change
if verified_timestamp < VERIFY_TIMESTAMP_THRESHOLD:
msg = (
f"Not enough Timestamp validated to meet the Validation "
f"Threshold ({verified_timestamp}/{VERIFY_TIMESTAMP_THRESHOLD})"
)
raise VerificationError(msg)
verified_timestamps = self._establish_time(bundle)
if not verified_timestamps:
raise VerificationError("not enough sources of verified time")

# (1): verify that the signing certificate is signed by the root
# certificate and that the signing certificate was valid at the
# time of signing.
sign_date = cert.not_valid_before_utc
cert_ossl = X509.from_cryptography(cert)

store.set_time(sign_date)
store_ctx = X509StoreContext(store, cert_ossl)
try:
# get_verified_chain returns the full chain including the end-entity certificate
# and chain should contain only CA certificates
chain = store_ctx.get_verified_chain()[1:]
except X509StoreContextError as e:
raise VerificationError(f"failed to build chain: {e}")
chain: list[X509] = []
for vts in verified_timestamps:
chain = self._verify_chain_at_time(cert_ossl, vts)

# (2): verify the signing certificate's SCT.
sct = _get_precertificate_signed_certificate_timestamps(cert)[0]
Expand Down
21 changes: 16 additions & 5 deletions test/unit/verify/test_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,17 @@ def test_verifier_verify_timestamp(self, verifier, asset, null_policy):
null_policy,
)

def test_verifier_without_timestamp(
self, verifier, asset, null_policy, monkeypatch
):
monkeypatch.setattr(verifier, "_establish_time", lambda *args: [])
with pytest.raises(VerificationError, match="not enough sources"):
verifier.verify_artifact(
asset("tsa/bundle.txt").read_bytes(),
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
null_policy,
)

def test_verifier_too_many_timestamp(self, verifier, asset, null_policy):
with pytest.raises(VerificationError, match="Too many"):
verifier.verify_artifact(
Expand All @@ -236,7 +247,7 @@ def test_verifier_no_validity(self, caplog, verifier, asset, null_policy):
]._inner.valid_for.end = None

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

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

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

with pytest.raises(VerificationError, match="No Timestamp Authorities"):
with pytest.raises(VerificationError, match="no Timestamp Authorities"):
verifier.verify_artifact(
asset("tsa/bundle.txt").read_bytes(),
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
Expand All @@ -302,7 +313,7 @@ def test_verifier_not_enough_timestamp(
self, verifier, asset, null_policy, monkeypatch
):
monkeypatch.setattr("sigstore.verify.verifier.VERIFY_TIMESTAMP_THRESHOLD", 2)
with pytest.raises(VerificationError, match="Not enough Timestamp"):
with pytest.raises(VerificationError, match="not enough timestamps"):
verifier.verify_artifact(
asset("tsa/bundle.txt").read_bytes(),
Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()),
Expand Down

0 comments on commit 6633b87

Please sign in to comment.