Skip to content

Commit 2f1707e

Browse files
Add signature verification
1 parent c7972b0 commit 2f1707e

File tree

2 files changed

+222
-23
lines changed

2 files changed

+222
-23
lines changed

src/snowflake/connector/crl.py

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from cryptography import x509
1313
from cryptography.hazmat._oid import ExtensionOID
1414
from cryptography.hazmat.backends import default_backend
15-
from cryptography.hazmat.primitives import serialization
15+
from cryptography.hazmat.primitives import hashes, serialization
1616
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
1717
from OpenSSL.SSL import Connection as SSLConnection
1818

@@ -335,18 +335,35 @@ def traverse_chain(cert: x509.Certificate) -> CRLValidationResult | None:
335335
# found a trusted certificate
336336
logger.debug("Found trusted certificate: %s", cert.subject)
337337
return CRLValidationResult.UNREVOKED
338+
338339
if cert.issuer in self._trusted_ca:
339340
# issuer is trusted by OS
340341
logger.debug("Found certificate with trusted issuer: %s", cert.issuer)
341-
return self._validate_certificate_with_cache(
342+
if self._verify_certificate_signature(
342343
cert, self._trusted_ca[cert.issuer]
343-
)
344+
):
345+
return self._validate_certificate_with_cache(
346+
cert, self._trusted_ca[cert.issuer]
347+
)
348+
else:
349+
logger.debug(
350+
"Certificate signature verification failed: for %s, looking for other paths",
351+
cert,
352+
)
353+
344354
if cert.issuer in is_being_visited:
345355
# cycle detected - invalid path
346356
return None
347357

348358
valid_results: list[tuple[CRLValidationResult, x509.Certificate]] = []
349359
for ca_cert in subject_certificates[cert.issuer]:
360+
if not self._verify_certificate_signature(cert, ca_cert):
361+
logger.debug(
362+
"Certificate signature verification failed for %s, looking for other paths",
363+
cert,
364+
)
365+
continue
366+
350367
is_being_visited.add(cert.issuer)
351368
ca_result = traverse_chain(ca_cert)
352369
is_being_visited.remove(cert.issuer)
@@ -567,52 +584,94 @@ def _check_certificate_against_crl_url(
567584
# Check if certificate is revoked
568585
return self._check_certificate_against_crl(cert, crl)
569586

587+
def _verify_certificate_signature(
588+
self, cert: x509.Certificate, ca_cert: x509.Certificate
589+
) -> bool:
590+
"""Verify certificate signature with CA's public key"""
591+
logger.debug(
592+
"Verifying certificate signature of %s against %s public key", cert, ca_cert
593+
)
594+
return self._verify_signature(
595+
public_key=ca_cert.public_key(),
596+
signature=cert.signature,
597+
data=cert.tbs_certificate_bytes,
598+
hash_algorithm=cert.signature_hash_algorithm,
599+
signature_type="certificate signature",
600+
)
601+
570602
def _verify_crl_signature(
571603
self, crl: x509.CertificateRevocationList, ca_cert: x509.Certificate
572604
) -> bool:
573605
"""Verify CRL signature with CA's public key"""
574-
try:
575-
# Get the signature algorithm from the CRL
576-
signature_algorithm = crl.signature_algorithm_oid
577-
hash_algorithm = crl.signature_hash_algorithm
606+
# Get the signature algorithm from the CRL
607+
signature_algorithm = crl.signature_algorithm_oid
608+
hash_algorithm = crl.signature_hash_algorithm
609+
610+
logger.debug(
611+
"Verifying CRL signature with algorithm: %s, hash: %s",
612+
signature_algorithm,
613+
hash_algorithm,
614+
)
578615

579-
logger.debug(
580-
"Verifying CRL signature with algorithm: %s, hash: %s",
581-
signature_algorithm,
582-
hash_algorithm,
583-
)
616+
result = self._verify_signature(
617+
public_key=ca_cert.public_key(),
618+
signature=crl.signature,
619+
data=crl.tbs_certlist_bytes,
620+
hash_algorithm=hash_algorithm,
621+
signature_type="CRL signature",
622+
)
584623

585-
# Determine the appropriate padding based on the signature algorithm
586-
public_key = ca_cert.public_key()
624+
if result:
625+
logger.debug("CRL signature verification successful")
626+
return result
587627

628+
def _verify_signature(
629+
self,
630+
public_key: rsa.RSAPublicKey | ec.EllipticCurvePublicKey | Any,
631+
signature: bytes,
632+
data: bytes,
633+
hash_algorithm: hashes.HashAlgorithm,
634+
signature_type: str = "signature",
635+
) -> bool:
636+
"""Verify a signature using a public key
637+
638+
Args:
639+
public_key: The public key to use for verification
640+
signature: The signature to verify
641+
data: The data that was signed
642+
hash_algorithm: The hash algorithm used in the signature
643+
signature_type: Type of signature being verified (for logging)
644+
645+
Returns:
646+
bool: True if verification succeeds, False otherwise
647+
"""
648+
try:
588649
# Handle different key types with appropriate signature verification
589650
if isinstance(public_key, rsa.RSAPublicKey):
590651
# For RSA signatures, we need to use PKCS1v15 padding
591652
public_key.verify(
592-
crl.signature,
593-
crl.tbs_certlist_bytes,
653+
signature,
654+
data,
594655
padding.PKCS1v15(),
595656
hash_algorithm,
596657
)
597658
elif isinstance(public_key, ec.EllipticCurvePublicKey):
598659
# For EC signatures, use ECDSA algorithm
599660
public_key.verify(
600-
crl.signature,
601-
crl.tbs_certlist_bytes,
661+
signature,
662+
data,
602663
ec.ECDSA(hash_algorithm),
603664
)
604665
else:
605666
# For other key types (DSA, etc.), try without padding
606667
public_key.verify(
607-
crl.signature,
608-
crl.tbs_certlist_bytes,
668+
signature,
669+
data,
609670
hash_algorithm,
610671
)
611-
612-
logger.debug("CRL signature verification successful")
613672
return True
614673
except Exception as e:
615-
logger.warning("CRL signature verification failed: %s", e)
674+
logger.warning("%s verification failed: %s", signature_type.capitalize(), e)
616675
return False
617676

618677
def _check_certificate_against_crl(

test/unit/test_crl.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,146 @@ def test_is_short_lived_certificate(cert_gen, issue_date, validity_days, expecte
19301930
assert CRLValidator._is_short_lived_certificate(cert) == expected
19311931

19321932

1933+
def test_validate_certificate_signatures(cert_gen, session_manager):
1934+
"""Test that certificate validation fails with ERROR when signed by wrong key"""
1935+
# Create a certificate signed by the test CA
1936+
valid_cert = cert_gen.create_certificate_with_crl_distribution_points(
1937+
"CN=Test Server", []
1938+
)
1939+
1940+
# Create a different CA key pair
1941+
different_ca_key = rsa.generate_private_key(
1942+
public_exponent=65537, key_size=2048, backend=default_backend()
1943+
)
1944+
different_cert = (
1945+
x509.CertificateBuilder()
1946+
.subject_name(valid_cert.subject)
1947+
.issuer_name(cert_gen.ca_certificate.subject)
1948+
.public_key(cert_gen.ca_private_key.public_key())
1949+
.serial_number(x509.random_serial_number())
1950+
.not_valid_before(datetime.now(timezone.utc))
1951+
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=365))
1952+
.add_extension(
1953+
x509.BasicConstraints(ca=True, path_length=None),
1954+
critical=True,
1955+
)
1956+
.sign(different_ca_key, hashes.SHA256(), backend=default_backend())
1957+
)
1958+
short_lived_different_cert = (
1959+
x509.CertificateBuilder()
1960+
.subject_name(valid_cert.subject)
1961+
.issuer_name(cert_gen.ca_certificate.subject)
1962+
.public_key(different_ca_key.public_key())
1963+
.serial_number(x509.random_serial_number())
1964+
.not_valid_before(datetime.now(timezone.utc))
1965+
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=3))
1966+
.add_extension(
1967+
x509.BasicConstraints(ca=True, path_length=None),
1968+
critical=True,
1969+
)
1970+
.sign(different_ca_key, hashes.SHA256(), backend=default_backend())
1971+
)
1972+
1973+
validator = CRLValidator(
1974+
session_manager,
1975+
cert_revocation_check_mode=CertRevocationCheckMode.ENABLED,
1976+
allow_certificates_without_crl_url=True,
1977+
trusted_certificates=[cert_gen.ca_certificate],
1978+
)
1979+
1980+
# wrong signature - no path found = ERROR
1981+
assert (
1982+
validator._validate_single_chain([different_cert]) == CRLValidationResult.ERROR
1983+
)
1984+
# wrong signature - short-lived - no path found = ERROR
1985+
assert (
1986+
validator._validate_single_chain([short_lived_different_cert])
1987+
== CRLValidationResult.ERROR
1988+
)
1989+
# wrong signature does not stop from searching of new path
1990+
assert (
1991+
validator._validate_single_chain(
1992+
[different_cert, short_lived_different_cert, valid_cert]
1993+
)
1994+
== CRLValidationResult.UNREVOKED
1995+
)
1996+
1997+
1998+
def test_validate_certificate_signatures_in_chain(cert_gen, session_manager):
1999+
"""Test that certificate validation fails with ERROR when signed by wrong key"""
2000+
# Create a certificate chain signed by the test CA: leaf -> A -> B -> CA
2001+
# mingle with A -> B
2002+
chain = cert_gen.create_cross_signed_chain()
2003+
2004+
valid_cert = chain.BsignA
2005+
2006+
# Create a different CA key pair
2007+
different_key = rsa.generate_private_key(
2008+
public_exponent=65537, key_size=2048, backend=default_backend()
2009+
)
2010+
different_cert = (
2011+
x509.CertificateBuilder()
2012+
.subject_name(valid_cert.subject)
2013+
.issuer_name(cert_gen.ca_certificate.subject)
2014+
.public_key(cert_gen.ca_private_key.public_key())
2015+
.serial_number(x509.random_serial_number())
2016+
.not_valid_before(datetime.now(timezone.utc))
2017+
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=365))
2018+
.add_extension(
2019+
x509.BasicConstraints(ca=True, path_length=None),
2020+
critical=True,
2021+
)
2022+
.sign(different_key, hashes.SHA256(), backend=default_backend())
2023+
)
2024+
short_lived_different_cert = (
2025+
x509.CertificateBuilder()
2026+
.subject_name(valid_cert.subject)
2027+
.issuer_name(cert_gen.ca_certificate.subject)
2028+
.public_key(different_key.public_key())
2029+
.serial_number(x509.random_serial_number())
2030+
.not_valid_before(datetime.now(timezone.utc))
2031+
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=3))
2032+
.add_extension(
2033+
x509.BasicConstraints(ca=True, path_length=None),
2034+
critical=True,
2035+
)
2036+
.sign(different_key, hashes.SHA256(), backend=default_backend())
2037+
)
2038+
2039+
validator = CRLValidator(
2040+
session_manager,
2041+
allow_certificates_without_crl_url=True,
2042+
cert_revocation_check_mode=CertRevocationCheckMode.ENABLED,
2043+
trusted_certificates=[cert_gen.ca_certificate],
2044+
)
2045+
2046+
# wrong signature - no path found = ERROR
2047+
assert (
2048+
validator._validate_single_chain([chain.leafA, different_cert, chain.rootB])
2049+
== CRLValidationResult.ERROR
2050+
)
2051+
# wrong signature - short-lived - no path found = ERROR
2052+
assert (
2053+
validator._validate_single_chain(
2054+
[chain.leafA, short_lived_different_cert, chain.rootB]
2055+
)
2056+
== CRLValidationResult.ERROR
2057+
)
2058+
# wrong signature does not stop from searching of new path
2059+
assert (
2060+
validator._validate_single_chain(
2061+
[
2062+
chain.leafA,
2063+
different_cert,
2064+
short_lived_different_cert,
2065+
valid_cert,
2066+
chain.rootB,
2067+
]
2068+
)
2069+
== CRLValidationResult.UNREVOKED
2070+
)
2071+
2072+
19332073
def test_is_certificate_trusted_by_os(cert_gen):
19342074
"""Test OS certificate trust validation."""
19352075
# Create a test certificate chain

0 commit comments

Comments
 (0)