From db7dd61de3c6f7c8d66d5615cbfbcf5c085c4448 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Thu, 12 Jan 2023 12:32:52 +0800 Subject: [PATCH] Add Certificate.verify_signed_by (#8011) * Add Certificate.verify_signed_by Verify that the signature on a certificate was created by the private key belonging to another certificate's public key. This code does not validate anything else! It is not a path builder, general x509 validator, etc. * switch to issued_by validate issuer subject matches certificate issuer and refactor * two fixes * signed_by isn't the right target now * coverage * skip test on some *ssls * extensive refactoring * lol * does any of this work * final commit i swear --- CHANGELOG.rst | 2 + docs/x509/reference.rst | 29 ++++ src/cryptography/x509/base.py | 8 + src/rust/src/x509/certificate.rs | 26 +++- src/rust/src/x509/sign.rs | 253 +++++++++++++++++++++++++++++++ tests/x509/test_x509.py | 235 +++++++++++++++++++++++++++- 6 files changed, 550 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d252cd2d..fbb9bb404 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,8 @@ Changelog continues to support only public keys. * Added support for generating SSH certificates with :class:`~cryptography.hazmat.primitives.serialization.SSHCertificateBuilder`. +* Added :meth:`~cryptography.x509.Certificate.verify_directly_issued_by` to + :class:`~cryptography.x509.Certificate`. .. _v39-0-0: diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index 839bce21d..86a0e4e8e 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -486,6 +486,35 @@ X.509 Certificate Object An :class:`~cryptography.exceptions.InvalidSignature` exception will be raised if the signature fails to verify. + .. method:: verify_directly_issued_by(issuer) + + .. versionadded:: 40.0 + + :param issuer: The issuer certificate to check against. + :type issuer: :class:`~cryptography.x509.Certificate` + + .. warning:: + This method verifies that the certificate issuer name matches the + issuer subject name and that the certificate is signed by the + issuer's private key. **No other validation is performed.** + Callers are responsible for performing any additional + validations required for their use case (e.g. checking the validity + period, whether the signer is allowed to issue certificates, + that the issuing certificate has a strong public key, etc). + + Validates that the certificate is signed by the provided issuer and + that the issuer's subject name matches the issuer name of the + certificate. + + :return: None + :raise ValueError: If the issuer name on the certificate does + not match the subject name of the issuer or the signature + algorithm is unsupported. + :raise TypeError: If the issuer does not have a supported public + key type. + :raise cryptography.exceptions.InvalidSignature: If the + signature fails to verify. + .. attribute:: tbs_precertificate_bytes diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py index 6eae41cbe..9b436fdf8 100644 --- a/src/cryptography/x509/base.py +++ b/src/cryptography/x509/base.py @@ -265,6 +265,14 @@ class Certificate(metaclass=abc.ABCMeta): Serializes the certificate to PEM or DER format. """ + @abc.abstractmethod + def verify_directly_issued_by(self, issuer: "Certificate") -> None: + """ + This method verifies that certificate issuer name matches the + issuer subject name and that the certificate is signed by the + issuer's private key. No other validation is performed. + """ + # Runtime isinstance checks need this since the rust class is not a subclass. Certificate.register(rust_x509.Certificate) diff --git a/src/rust/src/x509/certificate.rs b/src/rust/src/x509/certificate.rs index 92e522f45..65c37d334 100644 --- a/src/rust/src/x509/certificate.rs +++ b/src/rust/src/x509/certificate.rs @@ -7,7 +7,7 @@ use crate::asn1::{ PyAsn1Error, PyAsn1Result, }; use crate::x509; -use crate::x509::{crl, extensions, oid, sct, Asn1ReadableOrWritable}; +use crate::x509::{crl, extensions, oid, sct, sign, Asn1ReadableOrWritable}; use chrono::Datelike; use pyo3::ToPyObject; use std::collections::hash_map::DefaultHasher; @@ -319,6 +319,30 @@ impl Certificate { }, ) } + + fn verify_directly_issued_by<'p>( + &self, + py: pyo3::Python<'p>, + issuer: pyo3::PyRef<'_, Certificate>, + ) -> PyAsn1Result<()> { + if self.raw.borrow_value().tbs_cert.signature_alg != self.raw.borrow_value().signature_alg { + return Err(PyAsn1Error::from(pyo3::exceptions::PyValueError::new_err( + "Inner and outer signature algorithms do not match. This is an invalid certificate." + ))); + }; + if self.raw.borrow_value().tbs_cert.issuer != issuer.raw.borrow_value().tbs_cert.subject { + return Err(PyAsn1Error::from(pyo3::exceptions::PyValueError::new_err( + "Issuer certificate subject does not match certificate issuer.", + ))); + }; + sign::verify_signature_with_oid( + py, + issuer.public_key(py)?, + &self.raw.borrow_value().signature_alg.oid, + self.raw.borrow_value().signature.as_bytes(), + &asn1::write_single(&self.raw.borrow_value().tbs_cert)?, + ) + } } fn cert_version(py: pyo3::Python<'_>, version: u8) -> Result<&pyo3::PyAny, PyAsn1Error> { diff --git a/src/rust/src/x509/sign.rs b/src/rust/src/x509/sign.rs index 37860c3a5..bc5f07994 100644 --- a/src/rust/src/x509/sign.rs +++ b/src/rust/src/x509/sign.rs @@ -2,6 +2,7 @@ // 2.0, and the BSD License. See the LICENSE file in the root of this repository // for complete details. +use crate::asn1::{PyAsn1Error, PyAsn1Result}; use crate::x509; use crate::x509::oid; @@ -14,6 +15,7 @@ static NULL_DER: Lazy> = Lazy::new(|| { pub(crate) static NULL_TLV: Lazy> = Lazy::new(|| asn1::parse_single(&NULL_DER).unwrap()); +#[derive(Debug, PartialEq)] enum KeyType { Rsa, Dsa, @@ -22,6 +24,7 @@ enum KeyType { Ed448, } +#[derive(Debug, PartialEq)] enum HashType { None, Sha224, @@ -256,3 +259,253 @@ pub(crate) fn sign_data<'p>( }; signature.extract() } + +fn py_hash_name_from_hash_type(hash_type: HashType) -> Option<&'static str> { + match hash_type { + HashType::None => None, + HashType::Sha224 => Some("SHA224"), + HashType::Sha256 => Some("SHA256"), + HashType::Sha384 => Some("SHA384"), + HashType::Sha512 => Some("SHA512"), + HashType::Sha3_224 => Some("SHA3_224"), + HashType::Sha3_256 => Some("SHA3_256"), + HashType::Sha3_384 => Some("SHA3_384"), + HashType::Sha3_512 => Some("SHA3_512"), + } +} + +pub(crate) fn verify_signature_with_oid<'p>( + py: pyo3::Python<'p>, + issuer_public_key: &'p pyo3::PyAny, + signature_oid: &asn1::ObjectIdentifier, + signature: &[u8], + data: &[u8], +) -> PyAsn1Result<()> { + let key_type = identify_public_key_type(py, issuer_public_key)?; + let (sig_key_type, sig_hash_type) = identify_key_hash_type_for_oid(signature_oid)?; + if key_type != sig_key_type { + return Err(PyAsn1Error::from(pyo3::exceptions::PyValueError::new_err( + "Signature algorithm does not match issuer key type", + ))); + } + let sig_hash_name = py_hash_name_from_hash_type(sig_hash_type); + let hashes = py.import("cryptography.hazmat.primitives.hashes")?; + let signature_hash = match sig_hash_name { + Some(data) => hashes.getattr(data)?.call0()?, + None => py.None().into_ref(py), + }; + + match key_type { + KeyType::Ed25519 | KeyType::Ed448 => { + issuer_public_key.call_method1("verify", (signature, data))? + } + KeyType::Ec => { + let ec_mod = py.import("cryptography.hazmat.primitives.asymmetric.ec")?; + let ecdsa = ec_mod + .getattr(crate::intern!(py, "ECDSA"))? + .call1((signature_hash,))?; + issuer_public_key.call_method1("verify", (signature, data, ecdsa))? + } + KeyType::Rsa => { + let padding_mod = py.import("cryptography.hazmat.primitives.asymmetric.padding")?; + let pkcs1v15 = padding_mod + .getattr(crate::intern!(py, "PKCS1v15"))? + .call0()?; + issuer_public_key.call_method1("verify", (signature, data, pkcs1v15, signature_hash))? + } + KeyType::Dsa => { + issuer_public_key.call_method1("verify", (signature, data, signature_hash))? + } + }; + Ok(()) +} + +fn identify_public_key_type( + py: pyo3::Python<'_>, + public_key: &pyo3::PyAny, +) -> pyo3::PyResult { + let rsa_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.rsa")? + .getattr(crate::intern!(py, "RSAPublicKey"))? + .extract()?; + let dsa_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.dsa")? + .getattr(crate::intern!(py, "DSAPublicKey"))? + .extract()?; + let ec_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.ec")? + .getattr(crate::intern!(py, "EllipticCurvePublicKey"))? + .extract()?; + let ed25519_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.ed25519")? + .getattr(crate::intern!(py, "Ed25519PublicKey"))? + .extract()?; + let ed448_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.ed448")? + .getattr(crate::intern!(py, "Ed448PublicKey"))? + .extract()?; + + if rsa_key_type.is_instance(public_key)? { + Ok(KeyType::Rsa) + } else if dsa_key_type.is_instance(public_key)? { + Ok(KeyType::Dsa) + } else if ec_key_type.is_instance(public_key)? { + Ok(KeyType::Ec) + } else if ed25519_key_type.is_instance(public_key)? { + Ok(KeyType::Ed25519) + } else if ed448_key_type.is_instance(public_key)? { + Ok(KeyType::Ed448) + } else { + Err(pyo3::exceptions::PyTypeError::new_err( + "Key must be an rsa, dsa, ec, ed25519, or ed448 public key.", + )) + } +} + +fn identify_key_hash_type_for_oid( + oid: &asn1::ObjectIdentifier, +) -> pyo3::PyResult<(KeyType, HashType)> { + match *oid { + oid::RSA_WITH_SHA224_OID => Ok((KeyType::Rsa, HashType::Sha224)), + oid::RSA_WITH_SHA256_OID => Ok((KeyType::Rsa, HashType::Sha256)), + oid::RSA_WITH_SHA384_OID => Ok((KeyType::Rsa, HashType::Sha384)), + oid::RSA_WITH_SHA512_OID => Ok((KeyType::Rsa, HashType::Sha512)), + oid::RSA_WITH_SHA3_224_OID => Ok((KeyType::Rsa, HashType::Sha3_224)), + oid::RSA_WITH_SHA3_256_OID => Ok((KeyType::Rsa, HashType::Sha3_256)), + oid::RSA_WITH_SHA3_384_OID => Ok((KeyType::Rsa, HashType::Sha3_384)), + oid::RSA_WITH_SHA3_512_OID => Ok((KeyType::Rsa, HashType::Sha3_512)), + oid::ECDSA_WITH_SHA224_OID => Ok((KeyType::Ec, HashType::Sha224)), + oid::ECDSA_WITH_SHA256_OID => Ok((KeyType::Ec, HashType::Sha256)), + oid::ECDSA_WITH_SHA384_OID => Ok((KeyType::Ec, HashType::Sha384)), + oid::ECDSA_WITH_SHA512_OID => Ok((KeyType::Ec, HashType::Sha512)), + oid::ECDSA_WITH_SHA3_224_OID => Ok((KeyType::Ec, HashType::Sha3_224)), + oid::ECDSA_WITH_SHA3_256_OID => Ok((KeyType::Ec, HashType::Sha3_256)), + oid::ECDSA_WITH_SHA3_384_OID => Ok((KeyType::Ec, HashType::Sha3_384)), + oid::ECDSA_WITH_SHA3_512_OID => Ok((KeyType::Ec, HashType::Sha3_512)), + oid::ED25519_OID => Ok((KeyType::Ed25519, HashType::None)), + oid::ED448_OID => Ok((KeyType::Ed448, HashType::None)), + oid::DSA_WITH_SHA224_OID => Ok((KeyType::Dsa, HashType::Sha224)), + oid::DSA_WITH_SHA256_OID => Ok((KeyType::Dsa, HashType::Sha256)), + oid::DSA_WITH_SHA384_OID => Ok((KeyType::Dsa, HashType::Sha384)), + oid::DSA_WITH_SHA512_OID => Ok((KeyType::Dsa, HashType::Sha512)), + _ => Err(pyo3::exceptions::PyValueError::new_err( + "Unsupported signature algorithm", + )), + } +} + +#[cfg(test)] +mod tests { + use super::{identify_key_hash_type_for_oid, py_hash_name_from_hash_type, HashType, KeyType}; + use crate::x509::oid; + + #[test] + fn test_identify_key_hash_type_for_oid() { + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA224_OID).unwrap(), + (KeyType::Rsa, HashType::Sha224) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA256_OID).unwrap(), + (KeyType::Rsa, HashType::Sha256) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA384_OID).unwrap(), + (KeyType::Rsa, HashType::Sha384) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA512_OID).unwrap(), + (KeyType::Rsa, HashType::Sha512) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA3_224_OID).unwrap(), + (KeyType::Rsa, HashType::Sha3_224) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA3_256_OID).unwrap(), + (KeyType::Rsa, HashType::Sha3_256) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA3_384_OID).unwrap(), + (KeyType::Rsa, HashType::Sha3_384) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::RSA_WITH_SHA3_512_OID).unwrap(), + (KeyType::Rsa, HashType::Sha3_512) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA224_OID).unwrap(), + (KeyType::Ec, HashType::Sha224) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA256_OID).unwrap(), + (KeyType::Ec, HashType::Sha256) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA384_OID).unwrap(), + (KeyType::Ec, HashType::Sha384) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA512_OID).unwrap(), + (KeyType::Ec, HashType::Sha512) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA3_224_OID).unwrap(), + (KeyType::Ec, HashType::Sha3_224) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA3_256_OID).unwrap(), + (KeyType::Ec, HashType::Sha3_256) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA3_384_OID).unwrap(), + (KeyType::Ec, HashType::Sha3_384) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ECDSA_WITH_SHA3_512_OID).unwrap(), + (KeyType::Ec, HashType::Sha3_512) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ED25519_OID).unwrap(), + (KeyType::Ed25519, HashType::None) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::ED448_OID).unwrap(), + (KeyType::Ed448, HashType::None) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::DSA_WITH_SHA224_OID).unwrap(), + (KeyType::Dsa, HashType::Sha224) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::DSA_WITH_SHA256_OID).unwrap(), + (KeyType::Dsa, HashType::Sha256) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::DSA_WITH_SHA384_OID).unwrap(), + (KeyType::Dsa, HashType::Sha384) + ); + assert_eq!( + identify_key_hash_type_for_oid(&oid::DSA_WITH_SHA512_OID).unwrap(), + (KeyType::Dsa, HashType::Sha512) + ); + assert!(identify_key_hash_type_for_oid(&oid::TLS_FEATURE_OID).is_err()); + } + + #[test] + fn test_py_hash_name_from_hash_type() { + for (hash, name) in [ + (HashType::Sha224, "SHA224"), + (HashType::Sha256, "SHA256"), + (HashType::Sha384, "SHA384"), + (HashType::Sha512, "SHA512"), + (HashType::Sha3_224, "SHA3_224"), + (HashType::Sha3_256, "SHA3_256"), + (HashType::Sha3_384, "SHA3_384"), + (HashType::Sha3_512, "SHA3_512"), + ] { + let hash_str = py_hash_name_from_hash_type(hash).unwrap(); + assert_eq!(hash_str, name); + } + } +} diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index cc11e3aa2..b0584c3bf 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -15,6 +15,7 @@ import pytest import pytz from cryptography import utils, x509 +from cryptography.exceptions import InvalidSignature from cryptography.hazmat.bindings._rust import asn1 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ( @@ -25,6 +26,7 @@ from cryptography.hazmat.primitives.asymmetric import ( ed25519, padding, rsa, + types, x448, x25519, ) @@ -41,9 +43,13 @@ from cryptography.x509.oid import ( SubjectInformationAccessOID, ) -from ..hazmat.primitives.fixtures_dsa import DSA_KEY_2048 +from ..hazmat.primitives.fixtures_dsa import DSA_KEY_2048, DSA_KEY_3072 from ..hazmat.primitives.fixtures_ec import EC_KEY_SECP256R1 -from ..hazmat.primitives.fixtures_rsa import RSA_KEY_512, RSA_KEY_2048 +from ..hazmat.primitives.fixtures_rsa import ( + RSA_KEY_512, + RSA_KEY_2048, + RSA_KEY_2048_ALT, +) from ..hazmat.primitives.test_ec import _skip_curve_unsupported from ..utils import ( load_nist_vectors, @@ -77,6 +83,57 @@ def _load_cert(filename, loader: typing.Callable[..., T], backend=None) -> T: return cert +def _generate_ca_and_leaf( + issuer_private_key: types.CERTIFICATE_PRIVATE_KEY_TYPES, + subject_private_key: types.CERTIFICATE_PRIVATE_KEY_TYPES, +): + if isinstance( + issuer_private_key, + (ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey), + ): + hash_alg = None + else: + hash_alg = hashes.SHA256() + + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PyCA CA")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PyCA CA")]) + ) + .public_key(issuer_private_key.public_key()) + .serial_number(1) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2030, 1, 1)) + ) + ca = builder.sign(issuer_private_key, hash_alg) + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "leaf")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PyCA CA")]) + ) + .public_key(subject_private_key.public_key()) + .serial_number(100) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2025, 1, 1)) + ) + cert = builder.sign(issuer_private_key, hash_alg) + return ca, cert + + +def _break_cert_sig(cert: x509.Certificate) -> x509.Certificate: + cert_bad_sig = bytearray(cert.public_bytes(serialization.Encoding.PEM)) + # Break the sig by mutating 5 bytes. This has a 2**-40 chance of + # not breaking the sig. Spin that roulette wheel. + cert_bad_sig[-40:-35] = 90, 90, 90, 90, 90 + return x509.load_pem_x509_certificate(bytes(cert_bad_sig)) + + class TestCertificateRevocationList: def test_load_pem_crl(self, backend): crl = _load_cert( @@ -1473,6 +1530,108 @@ class TestRSACertificate: [x509.TLSFeatureType.status_request] ) + def test_verify_directly_issued_by_rsa(self): + issuer_private_key = RSA_KEY_2048.private_key() + subject_private_key = RSA_KEY_2048_ALT.private_key() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert.verify_directly_issued_by(ca) + + def test_verify_directly_issued_by_rsa_bad_sig(self): + issuer_private_key = RSA_KEY_2048.private_key() + subject_private_key = RSA_KEY_2048_ALT.private_key() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert_bad_sig = _break_cert_sig(cert) + with pytest.raises(InvalidSignature): + cert_bad_sig.verify_directly_issued_by(ca) + + def test_verify_directly_issued_by_rsa_mismatched_inner_out_oid(self): + cert = _load_cert( + os.path.join( + "x509", "custom", "mismatch_inner_outer_sig_algorithm.der" + ), + x509.load_der_x509_certificate, + ) + with pytest.raises(ValueError) as exc: + cert.verify_directly_issued_by(cert) + + assert str(exc.value) == ( + "Inner and outer signature algorithms do not match. This is an " + "invalid certificate." + ) + + def test_verify_directly_issued_by_subject_issuer_mismatch(self): + cert = _load_cert( + os.path.join("x509", "cryptography.io.pem"), + x509.load_pem_x509_certificate, + ) + with pytest.raises(ValueError) as exc: + cert.verify_directly_issued_by(cert) + + assert str(exc.value) == ( + "Issuer certificate subject does not match certificate issuer." + ) + + def test_verify_directly_issued_by_algorithm_mismatch(self): + issuer_private_key = RSA_KEY_2048.private_key() + subject_private_key = RSA_KEY_2048_ALT.private_key() + _, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + # We need a CA with the same issuer DN but diff signature algorithm + secondary_issuer_key = ec.generate_private_key(ec.SECP256R1()) + ca2, _ = _generate_ca_and_leaf( + secondary_issuer_key, subject_private_key + ) + with pytest.raises(ValueError): + cert.verify_directly_issued_by(ca2) + + @pytest.mark.supported( + only_if=lambda backend: ( + backend.ed25519_supported() and backend.x25519_supported() + ), + skip_message="Requires OpenSSL with Ed25519 and X25519 support", + ) + def test_verify_directly_issued_by_unsupported_key_type(self, backend): + private_key = ed25519.Ed25519PrivateKey.generate() + x25519_public = x25519.X25519PrivateKey.generate().public_key() + # Generate an ed25519 CA + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PyCA CA")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PyCA CA")]) + ) + .public_key(private_key.public_key()) + .serial_number(1) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2030, 1, 1)) + ) + cert = builder.sign(private_key, None) + # Make a cert with the right issuer name but the wrong public key + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PyCA CA")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PyCA CA")]) + ) + .public_key(x25519_public) + .serial_number(1) + .not_valid_before(datetime.datetime(2020, 1, 1)) + .not_valid_after(datetime.datetime(2030, 1, 1)) + ) + leaf = builder.sign(private_key, None) + + with pytest.raises(TypeError): + cert.verify_directly_issued_by(leaf) + class TestRSACertificateRequest: @pytest.mark.parametrize( @@ -4562,6 +4721,24 @@ class TestDSACertificate: cert.signature_hash_algorithm, ) + def test_verify_directly_issued_by_dsa(self): + issuer_private_key = DSA_KEY_3072.private_key() + subject_private_key = DSA_KEY_2048.private_key() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert.verify_directly_issued_by(ca) + + def test_verify_directly_issued_by_dsa_bad_sig(self): + issuer_private_key = DSA_KEY_3072.private_key() + subject_private_key = DSA_KEY_2048.private_key() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert_bad_sig = _break_cert_sig(cert) + with pytest.raises(InvalidSignature): + cert_bad_sig.verify_directly_issued_by(ca) + @pytest.mark.supported( only_if=lambda backend: backend.dsa_supported(), @@ -4788,6 +4965,24 @@ class TestECDSACertificate: with pytest.raises(ValueError, match="explicit parameters"): cert.public_key() + def test_verify_directly_issued_by_ec(self): + issuer_private_key = ec.generate_private_key(ec.SECP256R1()) + subject_private_key = ec.generate_private_key(ec.SECP256R1()) + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert.verify_directly_issued_by(ca) + + def test_verify_directly_issued_by_ec_bad_sig(self): + issuer_private_key = ec.generate_private_key(ec.SECP256R1()) + subject_private_key = ec.generate_private_key(ec.SECP256R1()) + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert_bad_sig = _break_cert_sig(cert) + with pytest.raises(InvalidSignature): + cert_bad_sig.verify_directly_issued_by(ca) + class TestECDSACertificateRequest: @pytest.mark.parametrize( @@ -5411,6 +5606,24 @@ class TestEd25519Certificate: ) assert copy.deepcopy(cert) is cert + def test_verify_directly_issued_by_ed25519(self, backend): + issuer_private_key = ed25519.Ed25519PrivateKey.generate() + subject_private_key = ed25519.Ed25519PrivateKey.generate() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert.verify_directly_issued_by(ca) + + def test_verify_directly_issued_by_ed25519_bad_sig(self, backend): + issuer_private_key = ed25519.Ed25519PrivateKey.generate() + subject_private_key = ed25519.Ed25519PrivateKey.generate() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert_bad_sig = _break_cert_sig(cert) + with pytest.raises(InvalidSignature): + cert_bad_sig.verify_directly_issued_by(ca) + @pytest.mark.supported( only_if=lambda backend: backend.ed448_supported(), @@ -5432,6 +5645,24 @@ class TestEd448Certificate: assert cert.signature_hash_algorithm is None assert cert.signature_algorithm_oid == SignatureAlgorithmOID.ED448 + def test_verify_directly_issued_by_ed448(self, backend): + issuer_private_key = ed448.Ed448PrivateKey.generate() + subject_private_key = ed448.Ed448PrivateKey.generate() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert.verify_directly_issued_by(ca) + + def test_verify_directly_issued_by_ed448_bad_sig(self, backend): + issuer_private_key = ed448.Ed448PrivateKey.generate() + subject_private_key = ed448.Ed448PrivateKey.generate() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + cert_bad_sig = _break_cert_sig(cert) + with pytest.raises(InvalidSignature): + cert_bad_sig.verify_directly_issued_by(ca) + @pytest.mark.supported( only_if=lambda backend: backend.dh_supported(),