Skip to content

Commit

Permalink
Timestamp Authority Verification (#1206)
Browse files Browse the repository at this point in the history
Co-authored-by: William Woodruff <william@trailofbits.com>
  • Loading branch information
DarkaMaul and woodruffw authored Nov 15, 2024
1 parent 9ca5639 commit 918f867
Showing 11 changed files with 693 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -16,6 +16,10 @@ All versions prior to 0.9.0 are untracked.
* API: Added `signature` property to `Envelope` class for accessing raw
signature bytes ([#1211](https://github.com/sigstore/sigstore-python/pull/1211))

* Signed timestamps embedded in bundles are now automatically verified
against Timestamp Authorities provided within the Trusted Root ([#1206]
(https://github.com/sigstore/sigstore-python/pull/1206))

### Fixed

* Fixed a CLI parsing bug introduced in 3.5.1 where a warning about
40 changes: 40 additions & 0 deletions sigstore/_internal/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 sources of signed time.
"""

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
12 changes: 12 additions & 0 deletions sigstore/models.py
Original file line number Diff line number Diff line change
@@ -568,6 +568,18 @@ def _dsse_envelope(self) -> dsse.Envelope | None:
return dsse.Envelope(self._inner.dsse_envelope)
return None

@property
def signature(self) -> bytes:
"""
Returns the signature bytes of this bundle.
Either from the DSSE Envelope or from the message itself.
"""
return (
self._dsse_envelope.signature
if self._dsse_envelope
else self._inner.message_signature.signature
)

@property
def verification_material(self) -> VerificationMaterial:
"""
186 changes: 175 additions & 11 deletions sigstore/verify/verifier.py
Original file line number Diff line number Diff line change
@@ -36,6 +36,8 @@
X509StoreFlags,
)
from pydantic import ValidationError
from rfc3161_client import TimeStampResponse, VerifierBuilder
from rfc3161_client import VerificationError as Rfc3161VerificationError

from sigstore import dsse
from sigstore._internal.rekor import _hashedrekord_from_parts
@@ -44,6 +46,7 @@
_get_precertificate_signed_certificate_timestamps,
verify_sct,
)
from sigstore._internal.timestamp import TimestampSource, TimestampVerificationResult
from sigstore._internal.trust import ClientTrustConfig, KeyringPurpose, TrustedRoot
from sigstore._utils import base64_encode_pem_cert, sha256_digest
from sigstore.errors import VerificationError
@@ -53,6 +56,14 @@

_logger = logging.getLogger(__name__)

# Limit the number of timestamps to prevent DoS
# From https://github.com/sigstore/sigstore-go/blob/e92142f0734064ebf6001f188b7330a1212245fe/pkg/verify/tsa.go#L29
MAX_ALLOWED_TIMESTAMP: int = 32

# When verifying a timestamp, this threshold represents the minimum number of required
# timestamps to consider a signature valid.
VERIFY_TIMESTAMP_THRESHOLD: int = 1


class Verifier:
"""
@@ -108,6 +119,155 @@ def _from_trust_config(cls, trust_config: ClientTrustConfig) -> Verifier:
trusted_root=trust_config.trusted_root,
)

def _verify_signed_timestamp(
self, timestamp_response: TimeStampResponse, signature: bytes
) -> TimestampVerificationResult | None:
"""
Verify a Signed Timestamp using the TSA provided by the Trusted Root.
"""
cert_authorities = self._trusted_root.get_timestamp_authorities()
for certificate_authority in cert_authorities:
certificates = certificate_authority.certificates(allow_expired=True)

builder = VerifierBuilder()
for certificate in certificates:
builder.add_root_certificate(certificate)

verifier = builder.build()
try:
verifier.verify(timestamp_response, signature)
except Rfc3161VerificationError as e:
_logger.debug("Unable to verify Timestamp with CA.")
_logger.exception(e)
continue

if (
certificate_authority.validity_period_start
and certificate_authority.validity_period_end
):
if (
certificate_authority.validity_period_start
<= timestamp_response.tst_info.gen_time
< certificate_authority.validity_period_end
):
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."
)
else:
_logger.debug(
"Unable to verify Timestamp because no validity provided."
)

return None

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.
Returns the number of valid signed timestamp in the bundle.
"""
timestamp_responses = (
bundle.verification_material.timestamp_verification_data.rfc3161_timestamps
)
if len(timestamp_responses) > MAX_ALLOWED_TIMESTAMP:
msg = f"Too many signed timestamp: {len(timestamp_responses)} > {MAX_ALLOWED_TIMESTAMP}"
raise VerificationError(msg)

if len(set(timestamp_responses)) != len(timestamp_responses):
msg = "Duplicate timestamp found"
raise VerificationError(msg)

# 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
verified_timestamps = []
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 the time for bundle verification.
This method uses timestamps from two possible sources:
1. RFC3161 signed timestamps from a Timestamping Authority (TSA)
2. Transparency Log timestamps
"""
verified_timestamps = []

# 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 time.
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
) -> None:
@@ -120,6 +280,7 @@ def _verify_common_signing_cert(

# In order to verify an artifact, we need to achieve the following:
#
# 0. Establish a time for the signature.
# 1. Verify that the signing certificate chains to the root of trust
# and is valid at the time of signing.
# 2. Verify the signing certificate's SCT.
@@ -135,7 +296,7 @@ def _verify_common_signing_cert(
# 8. Verify the transparency log entry's consistency against the other
# materials, to prevent variants of CVE-2022-36056.
#
# This method performs steps (1) through (6) above. Its caller
# This method performs steps (0) through (6) above. Its caller
# MUST perform steps (7) and (8) separately, since they vary based on
# the kind of verification being performed (i.e. hashedrekord, DSSE, etc.)

@@ -154,20 +315,23 @@ def _verify_common_signing_cert(
for parent_cert_ossl in self._fulcio_certificate_chain:
store.add_cert(parent_cert_ossl)

# (0): Establishing a Time for the Signature
# First, establish a time for the signature. This timestamp is required to
# validate the certificate chain, so this step comes first.
# 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.
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]
20 changes: 20 additions & 0 deletions test/assets/trusted_root/certificate_authority.missingroot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"subject": {
"organization": "GitHub, Inc.",
"commonName": "Internal Services Root"
},
"certChain": {
"certificates": [
{
"rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe"
},
{
"rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA=="
}
]
},
"validFor": {
"start": "2023-04-14T00:00:00.000Z",
"end": "2024-04-14T00:00:00.000Z"
}
}
Loading

0 comments on commit 918f867

Please sign in to comment.