diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9cb4fad3ab4e..608b0f9c09a4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,10 @@ Changelog We now only publish ``arm64`` wheels for macOS. * **BACKWARDS INCOMPATIBLE:** Support for 32-bit Windows has been removed. Users should move to a 64-bit Python installation. +* Added support for signing and verifying X.509 certificates, certificate + signing requests, and certificate revocation lists with + :doc:`/hazmat/primitives/asymmetric/mldsa` keys, as well as loading + certificates that contain ML-DSA public keys. .. _v48-0-0: diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index f877a15dbc12..2a5d64fc1c5c 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -1022,9 +1022,14 @@ X.509 Certificate Builder :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that will be used to generate the signature. This must be ``None`` if the ``private_key`` is an - :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey` - or an - :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey` + :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`, + an + :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`, + or an ML-DSA key (one of + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA44PrivateKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA65PrivateKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA87PrivateKey`), and an instance of a :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` otherwise. @@ -1311,9 +1316,14 @@ X.509 Certificate Revocation List Builder :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that will be used to generate the signature. This must be ``None`` if the ``private_key`` is an - :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey` - or an - :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey` + :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`, + an + :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`, + or an ML-DSA key (one of + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA44PrivateKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA65PrivateKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA87PrivateKey`), and an instance of a :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` otherwise. @@ -1538,9 +1548,14 @@ X.509 CSR (Certificate Signing Request) Builder Object :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that will be used to generate the request signature. This must be ``None`` if the ``private_key`` is an - :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey` - or an - :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey` + :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`, + an + :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`, + or an ML-DSA key (one of + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA44PrivateKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA65PrivateKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA87PrivateKey`), and an instance of a :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` otherwise. diff --git a/src/cryptography/hazmat/_oid.py b/src/cryptography/hazmat/_oid.py index 93c591dbf734..6849215f5cf4 100644 --- a/src/cryptography/hazmat/_oid.py +++ b/src/cryptography/hazmat/_oid.py @@ -153,6 +153,9 @@ class SignatureAlgorithmOID: SignatureAlgorithmOID.DSA_WITH_SHA256: hashes.SHA256(), SignatureAlgorithmOID.ED25519: None, SignatureAlgorithmOID.ED448: None, + SignatureAlgorithmOID.ML_DSA_44: None, + SignatureAlgorithmOID.ML_DSA_65: None, + SignatureAlgorithmOID.ML_DSA_87: None, SignatureAlgorithmOID.GOSTR3411_94_WITH_3410_2001: None, SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_256: None, SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_512: None, diff --git a/src/cryptography/hazmat/primitives/asymmetric/types.py b/src/cryptography/hazmat/primitives/asymmetric/types.py index dfd12ce007a4..5d51a9d172e9 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/types.py +++ b/src/cryptography/hazmat/primitives/asymmetric/types.py @@ -76,6 +76,9 @@ rsa.RSAPrivateKey, dsa.DSAPrivateKey, ec.EllipticCurvePrivateKey, + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA87PrivateKey, ] CERTIFICATE_PRIVATE_KEY_TYPES = CertificateIssuerPrivateKeyTypes utils.deprecated( @@ -93,6 +96,9 @@ ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey, ed448.Ed448PublicKey, + mldsa.MLDSA44PublicKey, + mldsa.MLDSA65PublicKey, + mldsa.MLDSA87PublicKey, ] CERTIFICATE_ISSUER_PUBLIC_KEY_TYPES = CertificateIssuerPublicKeyTypes utils.deprecated( @@ -110,6 +116,9 @@ ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey, ed448.Ed448PublicKey, + mldsa.MLDSA44PublicKey, + mldsa.MLDSA65PublicKey, + mldsa.MLDSA87PublicKey, x25519.X25519PublicKey, x448.X448PublicKey, ] diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py index a11b8fe02b73..4b6c628b4cde 100644 --- a/src/cryptography/x509/base.py +++ b/src/cryptography/x509/base.py @@ -17,6 +17,7 @@ ec, ed448, ed25519, + mldsa, padding, rsa, x448, @@ -362,6 +363,9 @@ def public_key( ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey, ed448.Ed448PublicKey, + mldsa.MLDSA44PublicKey, + mldsa.MLDSA65PublicKey, + mldsa.MLDSA87PublicKey, x25519.X25519PublicKey, x448.X448PublicKey, ), @@ -369,7 +373,8 @@ def public_key( raise TypeError( "Expecting one of DSAPublicKey, RSAPublicKey," " EllipticCurvePublicKey, Ed25519PublicKey," - " Ed448PublicKey, X25519PublicKey, or " + " Ed448PublicKey, MLDSA44PublicKey, MLDSA65PublicKey," + " MLDSA87PublicKey, X25519PublicKey, or " "X448PublicKey." ) if self._public_key is not None: diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index 49d3650bf97e..06bf8634268a 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -450,6 +450,31 @@ pub static ED448_PUBLIC_KEY: LazyPyImport = LazyPyImport::new( &["Ed448PublicKey"], ); +pub static MLDSA44_PRIVATE_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mldsa", + &["MLDSA44PrivateKey"], +); +pub static MLDSA44_PUBLIC_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mldsa", + &["MLDSA44PublicKey"], +); +pub static MLDSA65_PRIVATE_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mldsa", + &["MLDSA65PrivateKey"], +); +pub static MLDSA65_PUBLIC_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mldsa", + &["MLDSA65PublicKey"], +); +pub static MLDSA87_PRIVATE_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mldsa", + &["MLDSA87PrivateKey"], +); +pub static MLDSA87_PUBLIC_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mldsa", + &["MLDSA87PublicKey"], +); + pub static DSA_PRIVATE_KEY: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.asymmetric.dsa", &["DSAPrivateKey"], diff --git a/src/rust/src/x509/sign.rs b/src/rust/src/x509/sign.rs index 82714f449ac3..d844f95ba916 100644 --- a/src/rust/src/x509/sign.rs +++ b/src/rust/src/x509/sign.rs @@ -40,6 +40,9 @@ pub(crate) enum KeyType { Ec, Ed25519, Ed448, + MlDsa44, + MlDsa65, + MlDsa87, } enum HashType { @@ -68,9 +71,16 @@ pub(crate) fn identify_key_type( Ok(KeyType::Ed25519) } else if private_key.is_instance(&types::ED448_PRIVATE_KEY.get(py)?)? { Ok(KeyType::Ed448) + } else if private_key.is_instance(&types::MLDSA44_PRIVATE_KEY.get(py)?)? { + Ok(KeyType::MlDsa44) + } else if private_key.is_instance(&types::MLDSA65_PRIVATE_KEY.get(py)?)? { + Ok(KeyType::MlDsa65) + } else if private_key.is_instance(&types::MLDSA87_PRIVATE_KEY.get(py)?)? { + Ok(KeyType::MlDsa87) } else { Err(pyo3::exceptions::PyTypeError::new_err( - "Key must be an rsa, dsa, ec, ed25519, or ed448 private key.", + "Key must be an rsa, dsa, ec, ed25519, ed448, ml-dsa-44, \ + ml-dsa-65, or ml-dsa-87 private key.", )) } } @@ -190,6 +200,24 @@ pub(crate) fn compute_signature_algorithm<'p>( "Algorithm must be None when signing via ed25519 or ed448", )), + (KeyType::MlDsa44, HashType::None) => Ok(common::AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: common::AlgorithmParameters::MlDsa44, + }), + (KeyType::MlDsa65, HashType::None) => Ok(common::AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: common::AlgorithmParameters::MlDsa65, + }), + (KeyType::MlDsa87, HashType::None) => Ok(common::AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: common::AlgorithmParameters::MlDsa87, + }), + (KeyType::MlDsa44 | KeyType::MlDsa65 | KeyType::MlDsa87, _) => { + Err(pyo3::exceptions::PyValueError::new_err( + "Algorithm must be None when signing via ml-dsa-44, ml-dsa-65, or ml-dsa-87", + )) + } + (KeyType::Ec, HashType::Sha224) => Ok(common::AlgorithmIdentifier { oid: asn1::DefinedByMarker::marker(), params: common::AlgorithmParameters::EcDsaWithSha224(None), @@ -295,9 +323,11 @@ pub(crate) fn sign_data<'p>( let key_type = identify_key_type(py, private_key.clone())?; let signature = match key_type { - KeyType::Ed25519 | KeyType::Ed448 => { - private_key.call_method1(pyo3::intern!(py, "sign"), (data,))? - } + KeyType::Ed25519 + | KeyType::Ed448 + | KeyType::MlDsa44 + | KeyType::MlDsa65 + | KeyType::MlDsa87 => private_key.call_method1(pyo3::intern!(py, "sign"), (data,))?, KeyType::Ec => { let ecdsa = types::ECDSA .get(py)? @@ -338,7 +368,11 @@ pub(crate) fn verify_signature_with_signature_algorithm<'p>( identify_signature_algorithm_parameters(py, signature_algorithm)?; let py_signature_hash_algorithm = identify_signature_hash_algorithm(py, signature_algorithm)?; match key_type { - KeyType::Ed25519 | KeyType::Ed448 => { + KeyType::Ed25519 + | KeyType::Ed448 + | KeyType::MlDsa44 + | KeyType::MlDsa65 + | KeyType::MlDsa87 => { issuer_public_key.call_method1(pyo3::intern!(py, "verify"), (signature, data))? } KeyType::Ec => issuer_public_key.call_method1( @@ -376,9 +410,16 @@ pub(crate) fn identify_public_key_type( Ok(KeyType::Ed25519) } else if public_key.is_instance(&types::ED448_PUBLIC_KEY.get(py)?)? { Ok(KeyType::Ed448) + } else if public_key.is_instance(&types::MLDSA44_PUBLIC_KEY.get(py)?)? { + Ok(KeyType::MlDsa44) + } else if public_key.is_instance(&types::MLDSA65_PUBLIC_KEY.get(py)?)? { + Ok(KeyType::MlDsa65) + } else if public_key.is_instance(&types::MLDSA87_PUBLIC_KEY.get(py)?)? { + Ok(KeyType::MlDsa87) } else { Err(pyo3::exceptions::PyTypeError::new_err( - "Key must be an rsa, dsa, ec, ed25519, or ed448 public key.", + "Key must be an rsa, dsa, ec, ed25519, ed448, ml-dsa-44, \ + ml-dsa-65, or ml-dsa-87 public key.", )) } } @@ -406,6 +447,9 @@ fn identify_key_type_for_algorithm_params( | common::AlgorithmParameters::EcDsaWithSha3_512 => Ok(KeyType::Ec), common::AlgorithmParameters::Ed25519 => Ok(KeyType::Ed25519), common::AlgorithmParameters::Ed448 => Ok(KeyType::Ed448), + common::AlgorithmParameters::MlDsa44 => Ok(KeyType::MlDsa44), + common::AlgorithmParameters::MlDsa65 => Ok(KeyType::MlDsa65), + common::AlgorithmParameters::MlDsa87 => Ok(KeyType::MlDsa87), common::AlgorithmParameters::DsaWithSha224(..) | common::AlgorithmParameters::DsaWithSha256(..) | common::AlgorithmParameters::DsaWithSha384(..) @@ -594,6 +638,9 @@ mod tests { (&common::AlgorithmParameters::EcDsaWithSha3_512, KeyType::Ec), (&common::AlgorithmParameters::Ed25519, KeyType::Ed25519), (&common::AlgorithmParameters::Ed448, KeyType::Ed448), + (&common::AlgorithmParameters::MlDsa44, KeyType::MlDsa44), + (&common::AlgorithmParameters::MlDsa65, KeyType::MlDsa65), + (&common::AlgorithmParameters::MlDsa87, KeyType::MlDsa87), ( &common::AlgorithmParameters::DsaWithSha224(None), KeyType::Dsa, diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index c7c7ef3a94c1..b052846cfa5e 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -22,6 +22,7 @@ ec, ed448, ed25519, + mldsa, padding, rsa, types, @@ -94,7 +95,13 @@ def _generate_ca_and_leaf( ): if isinstance( issuer_private_key, - (ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey), + ( + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA87PrivateKey, + ), ): hash_alg = None else: @@ -3404,6 +3411,37 @@ def test_sign_with_unsupported_hash_ed448(self, backend): with pytest.raises(ValueError): builder.sign(private_key, hashes.SHA256(), backend) + @pytest.mark.supported( + only_if=lambda backend: backend.mldsa_supported(), + skip_message="Requires a backend with ML-DSA support", + ) + @pytest.mark.parametrize( + "priv_key_cls", + [ + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA87PrivateKey, + ], + ) + def test_sign_with_unsupported_hash_mldsa(self, priv_key_cls, backend): + private_key = priv_key_cls.generate() + builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .serial_number(1) + .public_key(private_key.public_key()) + .not_valid_before(datetime.datetime(2002, 1, 1, 12, 1)) + .not_valid_after(datetime.datetime(2032, 1, 1, 12, 1)) + ) + + with pytest.raises(ValueError): + builder.sign(private_key, hashes.SHA256(), backend) + @pytest.mark.supported( only_if=lambda backend: backend.hash_supported(hashes.MD5()), skip_message="Requires OpenSSL with MD5 support", @@ -3923,6 +3961,112 @@ def test_build_cert_with_public_ed448_rsa_sig( assert isinstance(cert.public_key(), ed448.Ed448PublicKey) assert cert.public_key_algorithm_oid == PublicKeyAlgorithmOID.ED448 + @pytest.mark.supported( + only_if=lambda backend: backend.mldsa_supported(), + skip_message="Requires a backend with ML-DSA support", + ) + @pytest.mark.parametrize( + ("priv_key_cls", "pub_key_cls", "sig_oid", "pub_oid"), + [ + ( + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA44PublicKey, + SignatureAlgorithmOID.ML_DSA_44, + PublicKeyAlgorithmOID.ML_DSA_44, + ), + ( + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA65PublicKey, + SignatureAlgorithmOID.ML_DSA_65, + PublicKeyAlgorithmOID.ML_DSA_65, + ), + ( + mldsa.MLDSA87PrivateKey, + mldsa.MLDSA87PublicKey, + SignatureAlgorithmOID.ML_DSA_87, + PublicKeyAlgorithmOID.ML_DSA_87, + ), + ], + ) + def test_build_cert_with_mldsa( + self, priv_key_cls, pub_key_cls, sig_oid, pub_oid, backend + ): + issuer_private_key = priv_key_cls.generate() + subject_private_key = priv_key_cls.generate() + + not_valid_before = datetime.datetime(2002, 1, 1, 12, 1) + not_valid_after = datetime.datetime(2030, 12, 31, 8, 30) + + builder = ( + x509.CertificateBuilder() + .serial_number(777) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .public_key(subject_private_key.public_key()) + .add_extension( + x509.BasicConstraints(ca=False, path_length=None), + True, + ) + .not_valid_before(not_valid_before) + .not_valid_after(not_valid_after) + ) + + cert = builder.sign(issuer_private_key, None, backend) + issuer_private_key.public_key().verify( + cert.signature, cert.tbs_certificate_bytes + ) + assert cert.signature_algorithm_oid == sig_oid + assert cert.signature_hash_algorithm is None + assert cert.signature_algorithm_parameters is None + assert isinstance(cert.public_key(), pub_key_cls) + assert cert.public_key_algorithm_oid == pub_oid + assert cert.version is x509.Version.v3 + + @pytest.mark.supported( + only_if=lambda backend: backend.mldsa_supported(), + skip_message="Requires a backend with ML-DSA support", + ) + def test_build_cert_with_public_mldsa_rsa_sig( + self, rsa_key_2048: rsa.RSAPrivateKey, backend + ): + issuer_private_key = rsa_key_2048 + subject_private_key = mldsa.MLDSA65PrivateKey.generate() + + not_valid_before = datetime.datetime(2002, 1, 1, 12, 1) + not_valid_after = datetime.datetime(2030, 12, 31, 8, 30) + + builder = ( + x509.CertificateBuilder() + .serial_number(777) + .issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .subject_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")]) + ) + .public_key(subject_private_key.public_key()) + .not_valid_before(not_valid_before) + .not_valid_after(not_valid_after) + ) + + cert = builder.sign(issuer_private_key, hashes.SHA256(), backend) + assert cert.signature_hash_algorithm is not None + issuer_private_key.public_key().verify( + cert.signature, + cert.tbs_certificate_bytes, + padding.PKCS1v15(), + cert.signature_hash_algorithm, + ) + assert cert.signature_algorithm_oid == ( + SignatureAlgorithmOID.RSA_WITH_SHA256 + ) + assert isinstance(cert.public_key(), mldsa.MLDSA65PublicKey) + assert cert.public_key_algorithm_oid == PublicKeyAlgorithmOID.ML_DSA_65 + @pytest.mark.supported( only_if=lambda backend: ( backend.x25519_supported() and backend.x448_supported() @@ -4985,6 +5129,68 @@ def test_build_ca_request_with_ed448(self, backend): assert basic_constraints.value.ca is True assert basic_constraints.value.path_length == 2 + @pytest.mark.supported( + only_if=lambda backend: backend.mldsa_supported(), + skip_message="Requires a backend with ML-DSA support", + ) + @pytest.mark.parametrize( + ("priv_key_cls", "pub_key_cls", "sig_oid"), + [ + ( + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA44PublicKey, + SignatureAlgorithmOID.ML_DSA_44, + ), + ( + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA65PublicKey, + SignatureAlgorithmOID.ML_DSA_65, + ), + ( + mldsa.MLDSA87PrivateKey, + mldsa.MLDSA87PublicKey, + SignatureAlgorithmOID.ML_DSA_87, + ), + ], + ) + def test_build_ca_request_with_mldsa( + self, priv_key_cls, pub_key_cls, sig_oid, backend + ): + private_key = priv_key_cls.generate() + + request = ( + x509.CertificateSigningRequestBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.STATE_OR_PROVINCE_NAME, "Texas" + ), + ] + ) + ) + .add_extension( + x509.BasicConstraints(ca=True, path_length=2), critical=True + ) + .sign(private_key, None, backend) + ) + + assert request.is_signature_valid + assert request.signature_algorithm_oid == sig_oid + assert request.signature_hash_algorithm is None + public_key = request.public_key() + assert isinstance(public_key, pub_key_cls) + subject = request.subject + assert isinstance(subject, x509.Name) + assert list(subject) == [ + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Texas"), + ] + basic_constraints = request.extensions.get_extension_for_class( + x509.BasicConstraints + ) + assert basic_constraints.value.ca is True + assert basic_constraints.value.path_length == 2 + @pytest.mark.supported( only_if=lambda backend: backend.dsa_supported(), skip_message="Does not support DSA.", @@ -6619,6 +6825,57 @@ def test_verify_directly_issued_by_ed448_bad_sig(self, backend): cert_bad_sig.verify_directly_issued_by(ca) +@pytest.mark.supported( + only_if=lambda backend: backend.mldsa_supported(), + skip_message="Requires a backend with ML-DSA support", +) +@pytest.mark.parametrize( + ("priv_key_cls", "pub_key_cls", "sig_oid"), + [ + ( + mldsa.MLDSA44PrivateKey, + mldsa.MLDSA44PublicKey, + SignatureAlgorithmOID.ML_DSA_44, + ), + ( + mldsa.MLDSA65PrivateKey, + mldsa.MLDSA65PublicKey, + SignatureAlgorithmOID.ML_DSA_65, + ), + ( + mldsa.MLDSA87PrivateKey, + mldsa.MLDSA87PublicKey, + SignatureAlgorithmOID.ML_DSA_87, + ), + ], +) +class TestMLDSACertificate: + def test_sign_and_verify_directly_issued_by_mldsa( + self, priv_key_cls, pub_key_cls, sig_oid, backend + ): + issuer_private_key = priv_key_cls.generate() + subject_private_key = priv_key_cls.generate() + ca, cert = _generate_ca_and_leaf( + issuer_private_key, subject_private_key + ) + assert cert.signature_algorithm_oid == sig_oid + assert cert.signature_hash_algorithm is None + assert isinstance(cert.public_key(), pub_key_cls) + cert.verify_directly_issued_by(ca) + + def test_verify_directly_issued_by_mldsa_bad_sig( + self, priv_key_cls, pub_key_cls, sig_oid, backend + ): + issuer_private_key = priv_key_cls.generate() + subject_private_key = priv_key_cls.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(), skip_message="DH not supported", diff --git a/tests/x509/test_x509_crlbuilder.py b/tests/x509/test_x509_crlbuilder.py index deb17af4aa86..278d71684429 100644 --- a/tests/x509/test_x509_crlbuilder.py +++ b/tests/x509/test_x509_crlbuilder.py @@ -14,6 +14,7 @@ ec, ed448, ed25519, + mldsa, padding, rsa, ) @@ -812,6 +813,64 @@ def test_sign_ed448_key(self, backend): assert ext.critical is False assert ext.value == invalidity_date + @pytest.mark.supported( + only_if=lambda backend: backend.mldsa_supported(), + skip_message="Requires a backend with ML-DSA support", + ) + @pytest.mark.parametrize( + ("priv_key_cls", "sig_oid"), + [ + (mldsa.MLDSA44PrivateKey, SignatureAlgorithmOID.ML_DSA_44), + (mldsa.MLDSA65PrivateKey, SignatureAlgorithmOID.ML_DSA_65), + (mldsa.MLDSA87PrivateKey, SignatureAlgorithmOID.ML_DSA_87), + ], + ) + def test_sign_mldsa_key(self, priv_key_cls, sig_oid, backend): + private_key = priv_key_cls.generate() + invalidity_date = x509.InvalidityDate( + datetime.datetime(2002, 1, 1, 0, 0) + ) + ian = x509.IssuerAlternativeName( + [x509.UniformResourceIdentifier("https://cryptography.io")] + ) + revoked_cert0 = ( + x509.RevokedCertificateBuilder() + .serial_number(2) + .revocation_date(datetime.datetime(2012, 1, 1, 1, 1)) + .add_extension(invalidity_date, False) + .build(backend) + ) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = ( + x509.CertificateRevocationListBuilder() + .issuer_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, "cryptography.io CA" + ) + ] + ) + ) + .last_update(last_update) + .next_update(next_update) + .add_revoked_certificate(revoked_cert0) + .add_extension(ian, False) + ) + + crl = builder.sign(private_key, None, backend) + assert crl.signature_hash_algorithm is None + assert crl.signature_algorithm_oid == sig_oid + assert crl.is_signature_valid(private_key.public_key()) + assert ( + crl.extensions.get_extension_for_class( + x509.IssuerAlternativeName + ).value + == ian + ) + assert crl[0].serial_number == revoked_cert0.serial_number + def test_dsa_key_sign_md5(self, backend): private_key = DSA_KEY_2048.private_key(backend) last_time = datetime.datetime(2012, 1, 16, 22, 43)