Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ dependencies = [
"importlib_resources ~= 5.7; python_version < '3.11'",
"pydantic >= 2,< 3",
"pyjwt >= 2.1",
"pyOpenSSL >= 23.0.0",
"requests",
"rich >= 13,< 16",
"rfc8785 ~= 0.1.2",
Expand Down Expand Up @@ -66,7 +65,6 @@ dev = [
# NOTE(ww): ruff is under active development, so we pin conservatively here
# and let Dependabot periodically perform this update.
"ruff<0.15.23",
"types-pyOpenSSL",
"mkdocs-material[imaging]",
"mkdocstrings-python",
"bump >= 1.3.2",
Expand Down
89 changes: 39 additions & 50 deletions sigstore/verify/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,21 @@
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage
from cryptography.x509 import (
Certificate,
ExtendedKeyUsage,
KeyUsage,
UnsupportedGeneralNameType,
)
from cryptography.x509.oid import ExtendedKeyUsageOID
from OpenSSL.crypto import (
X509,
X509Store,
X509StoreContext,
X509StoreContextError,
X509StoreFlags,
from cryptography.x509.verification import (
Criticality,
ExtensionPolicy,
PolicyBuilder,
Store,
)
from cryptography.x509.verification import (
VerificationError as X509VerificationError,
)
from pydantic import ValidationError
from rfc3161_client import TimeStampResponse, VerifierBuilder
Expand Down Expand Up @@ -80,10 +87,7 @@ def __init__(self, *, trusted_root: TrustedRoot):
`trusted_root` is the `TrustedRoot` object containing the root of trust
for the verification process.
"""
self._fulcio_certificate_chain: list[X509] = [
X509.from_cryptography(parent_cert)
for parent_cert in trusted_root.get_fulcio_certs()
]
self._fulcio_certificate_chain = trusted_root.get_fulcio_certs()
self._trusted_root = trusted_root

# this is an ugly hack needed for verifying "detached" materials
Expand Down Expand Up @@ -244,35 +248,34 @@ def _establish_time(self, bundle: Bundle) -> list[TimestampVerificationResult]:
return verified_timestamps

def _verify_chain_at_time(
self, certificate: X509, timestamp_result: TimestampVerificationResult
) -> list[X509]:
self, certificate: Certificate, timestamp_result: TimestampVerificationResult
) -> list[Certificate]:
"""
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)
# Client verifiers normally require the client-auth EKU. Fulcio certificates
# instead use code-signing, which is checked separately below; overriding
Comment thread
facutuesca marked this conversation as resolved.
# only the EKU validators preserves the remaining default extension policies.
ca_policy = ExtensionPolicy.webpki_defaults_ca().may_be_present(
ExtendedKeyUsage, Criticality.NON_CRITICAL, None
)
ee_policy = ExtensionPolicy.webpki_defaults_ee().may_be_present(
ExtendedKeyUsage, Criticality.NON_CRITICAL, None
)
verifier = (
PolicyBuilder()
.store(Store(self._fulcio_certificate_chain))
.time(timestamp_result.time)
.extension_policies(ca_policy=ca_policy, ee_policy=ee_policy)
.build_client_verifier()
)

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:
# The verified chain includes the end-entity certificate, which callers omit.
return verifier.verify(certificate, []).chain[1:]
except (X509VerificationError, UnsupportedGeneralNameType) as e:
raise CertValidationError(
f"failed to build timestamp certificate chain: {e}"
)
Expand Down Expand Up @@ -311,19 +314,6 @@ def _verify_common_signing_cert(

cert = bundle.signing_certificate

# NOTE: The `X509Store` object currently cannot have its time reset once the `set_time`
# method been called on it. To get around this, we construct a new one for every `verify`
# 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)

# (0): Establishing a Time for the Signature
# First, establish verified times for the signature. This is required to
# validate the certificate chain, so this step comes first.
Expand All @@ -336,16 +326,15 @@ def _verify_common_signing_cert(
# (1): verify that the signing certificate is signed by the root
# certificate and that the signing certificate was valid at the
# time of signing.
cert_ossl = X509.from_cryptography(cert)
chain: list[X509] = []
chain: list[Certificate] = []
for vts in verified_timestamps:
chain = self._verify_chain_at_time(cert_ossl, vts)
chain = self._verify_chain_at_time(cert, vts)

# (2): verify the signing certificate's SCT.
try:
verify_sct(
cert,
[parent_cert.to_cryptography() for parent_cert in chain],
chain,
self._trusted_root.ct_keyring(KeyringPurpose.VERIFY),
)
except VerificationError as e:
Expand Down
14 changes: 13 additions & 1 deletion test/unit/verify/test_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from sigstore._internal.trust import CertificateAuthority
from sigstore.dsse import StatementBuilder, Subject
from sigstore.errors import VerificationError
from sigstore.errors import CertValidationError, VerificationError
from sigstore.models import Bundle
from sigstore.verify import policy
from sigstore.verify.verifier import Verifier
Expand Down Expand Up @@ -123,6 +123,18 @@ def test_verifier_bundle_offline(signing_bundle, null_policy, filename):
verifier.verify_artifact(file.read_bytes(), bundle, null_policy)


def test_verifier_certificate_chain_rejects_invalid_time(signing_bundle):
_, bundle = signing_bundle("bundle.txt")
verifier = Verifier.staging(offline=True)
timestamp = verifier._establish_time(bundle)[0]
timestamp.time = datetime(2000, 1, 1, tzinfo=timezone.utc)

with pytest.raises(
CertValidationError, match="failed to build timestamp certificate chain"
):
verifier._verify_chain_at_time(bundle.signing_certificate, timestamp)


@pytest.mark.staging
def test_verifier_email_identity(signing_materials):
verifier = Verifier.staging()
Expand Down
51 changes: 0 additions & 51 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading