Skip to content

Commit ddc47a0

Browse files
alexclaude
andauthored
Add ML-DSA support to X.509 signing and verification (#14889)
* Add ML-DSA support to X.509 signing and verification Allow ML-DSA-44/65/87 keys to sign and verify X.509 certificates, certificate signing requests, and certificate revocation lists, and to be used as certificate public keys. ML-DSA, like Ed25519/Ed448, is a hashless signature scheme, so signing requires algorithm=None. https://claude.ai/code/session_01ENDmAD4rsLTkCw1QG9txBL * Add coverage for ML-DSA hash-rejection path in cert signing The arm in compute_signature_algorithm that rejects a hash algorithm passed alongside an ML-DSA key was not exercised by any test, leaving sign.rs below 100% coverage. https://claude.ai/code/session_01ENDmAD4rsLTkCw1QG9txBL --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9f078ff commit ddc47a0

9 files changed

Lines changed: 441 additions & 17 deletions

File tree

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Changelog
1818
previously caused the build to fail during cross-compilations for embedded
1919
systems, on hosts which have same-version Python development headers
2020
installed as the target Python.
21+
* Added support for signing and verifying X.509 certificates, certificate
22+
signing requests, and certificate revocation lists with
23+
:doc:`/hazmat/primitives/asymmetric/mldsa` keys, as well as loading
24+
certificates that contain ML-DSA public keys.
2125

2226
.. _v48-0-0:
2327

docs/x509/reference.rst

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,9 +1022,14 @@ X.509 Certificate Builder
10221022
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that
10231023
will be used to generate the signature. This must be ``None`` if
10241024
the ``private_key`` is an
1025-
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`
1026-
or an
1027-
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`
1025+
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`,
1026+
an
1027+
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`,
1028+
or an ML-DSA key (one of
1029+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA44PrivateKey`,
1030+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA65PrivateKey`,
1031+
or
1032+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA87PrivateKey`),
10281033
and an instance of a
10291034
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
10301035
otherwise.
@@ -1311,9 +1316,14 @@ X.509 Certificate Revocation List Builder
13111316
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that
13121317
will be used to generate the signature.
13131318
This must be ``None`` if the ``private_key`` is an
1314-
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`
1315-
or an
1316-
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`
1319+
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`,
1320+
an
1321+
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`,
1322+
or an ML-DSA key (one of
1323+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA44PrivateKey`,
1324+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA65PrivateKey`,
1325+
or
1326+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA87PrivateKey`),
13171327
and an instance of a
13181328
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
13191329
otherwise.
@@ -1538,9 +1548,14 @@ X.509 CSR (Certificate Signing Request) Builder Object
15381548
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
15391549
that will be used to generate the request signature.
15401550
This must be ``None`` if the ``private_key`` is an
1541-
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`
1542-
or an
1543-
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`
1551+
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`,
1552+
an
1553+
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`,
1554+
or an ML-DSA key (one of
1555+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA44PrivateKey`,
1556+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA65PrivateKey`,
1557+
or
1558+
:class:`~cryptography.hazmat.primitives.asymmetric.mldsa.MLDSA87PrivateKey`),
15441559
and an instance of a
15451560
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
15461561
otherwise.

src/cryptography/hazmat/_oid.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ class SignatureAlgorithmOID:
153153
SignatureAlgorithmOID.DSA_WITH_SHA256: hashes.SHA256(),
154154
SignatureAlgorithmOID.ED25519: None,
155155
SignatureAlgorithmOID.ED448: None,
156+
SignatureAlgorithmOID.ML_DSA_44: None,
157+
SignatureAlgorithmOID.ML_DSA_65: None,
158+
SignatureAlgorithmOID.ML_DSA_87: None,
156159
SignatureAlgorithmOID.GOSTR3411_94_WITH_3410_2001: None,
157160
SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_256: None,
158161
SignatureAlgorithmOID.GOSTR3410_2012_WITH_3411_2012_512: None,

src/cryptography/hazmat/primitives/asymmetric/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
rsa.RSAPrivateKey,
7777
dsa.DSAPrivateKey,
7878
ec.EllipticCurvePrivateKey,
79+
mldsa.MLDSA44PrivateKey,
80+
mldsa.MLDSA65PrivateKey,
81+
mldsa.MLDSA87PrivateKey,
7982
]
8083
CERTIFICATE_PRIVATE_KEY_TYPES = CertificateIssuerPrivateKeyTypes
8184
utils.deprecated(
@@ -93,6 +96,9 @@
9396
ec.EllipticCurvePublicKey,
9497
ed25519.Ed25519PublicKey,
9598
ed448.Ed448PublicKey,
99+
mldsa.MLDSA44PublicKey,
100+
mldsa.MLDSA65PublicKey,
101+
mldsa.MLDSA87PublicKey,
96102
]
97103
CERTIFICATE_ISSUER_PUBLIC_KEY_TYPES = CertificateIssuerPublicKeyTypes
98104
utils.deprecated(
@@ -110,6 +116,9 @@
110116
ec.EllipticCurvePublicKey,
111117
ed25519.Ed25519PublicKey,
112118
ed448.Ed448PublicKey,
119+
mldsa.MLDSA44PublicKey,
120+
mldsa.MLDSA65PublicKey,
121+
mldsa.MLDSA87PublicKey,
113122
x25519.X25519PublicKey,
114123
x448.X448PublicKey,
115124
]

src/cryptography/x509/base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
ec,
1818
ed448,
1919
ed25519,
20+
mldsa,
2021
padding,
2122
rsa,
2223
x448,
@@ -362,14 +363,18 @@ def public_key(
362363
ec.EllipticCurvePublicKey,
363364
ed25519.Ed25519PublicKey,
364365
ed448.Ed448PublicKey,
366+
mldsa.MLDSA44PublicKey,
367+
mldsa.MLDSA65PublicKey,
368+
mldsa.MLDSA87PublicKey,
365369
x25519.X25519PublicKey,
366370
x448.X448PublicKey,
367371
),
368372
):
369373
raise TypeError(
370374
"Expecting one of DSAPublicKey, RSAPublicKey,"
371375
" EllipticCurvePublicKey, Ed25519PublicKey,"
372-
" Ed448PublicKey, X25519PublicKey, or "
376+
" Ed448PublicKey, MLDSA44PublicKey, MLDSA65PublicKey,"
377+
" MLDSA87PublicKey, X25519PublicKey, or "
373378
"X448PublicKey."
374379
)
375380
if self._public_key is not None:

src/rust/src/types.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,31 @@ pub static ED448_PUBLIC_KEY: LazyPyImport = LazyPyImport::new(
450450
&["Ed448PublicKey"],
451451
);
452452

453+
pub static MLDSA44_PRIVATE_KEY: LazyPyImport = LazyPyImport::new(
454+
"cryptography.hazmat.primitives.asymmetric.mldsa",
455+
&["MLDSA44PrivateKey"],
456+
);
457+
pub static MLDSA44_PUBLIC_KEY: LazyPyImport = LazyPyImport::new(
458+
"cryptography.hazmat.primitives.asymmetric.mldsa",
459+
&["MLDSA44PublicKey"],
460+
);
461+
pub static MLDSA65_PRIVATE_KEY: LazyPyImport = LazyPyImport::new(
462+
"cryptography.hazmat.primitives.asymmetric.mldsa",
463+
&["MLDSA65PrivateKey"],
464+
);
465+
pub static MLDSA65_PUBLIC_KEY: LazyPyImport = LazyPyImport::new(
466+
"cryptography.hazmat.primitives.asymmetric.mldsa",
467+
&["MLDSA65PublicKey"],
468+
);
469+
pub static MLDSA87_PRIVATE_KEY: LazyPyImport = LazyPyImport::new(
470+
"cryptography.hazmat.primitives.asymmetric.mldsa",
471+
&["MLDSA87PrivateKey"],
472+
);
473+
pub static MLDSA87_PUBLIC_KEY: LazyPyImport = LazyPyImport::new(
474+
"cryptography.hazmat.primitives.asymmetric.mldsa",
475+
&["MLDSA87PublicKey"],
476+
);
477+
453478
pub static DSA_PRIVATE_KEY: LazyPyImport = LazyPyImport::new(
454479
"cryptography.hazmat.primitives.asymmetric.dsa",
455480
&["DSAPrivateKey"],

src/rust/src/x509/sign.rs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ pub(crate) enum KeyType {
4040
Ec,
4141
Ed25519,
4242
Ed448,
43+
MlDsa44,
44+
MlDsa65,
45+
MlDsa87,
4346
}
4447

4548
enum HashType {
@@ -68,9 +71,16 @@ pub(crate) fn identify_key_type(
6871
Ok(KeyType::Ed25519)
6972
} else if private_key.is_instance(&types::ED448_PRIVATE_KEY.get(py)?)? {
7073
Ok(KeyType::Ed448)
74+
} else if private_key.is_instance(&types::MLDSA44_PRIVATE_KEY.get(py)?)? {
75+
Ok(KeyType::MlDsa44)
76+
} else if private_key.is_instance(&types::MLDSA65_PRIVATE_KEY.get(py)?)? {
77+
Ok(KeyType::MlDsa65)
78+
} else if private_key.is_instance(&types::MLDSA87_PRIVATE_KEY.get(py)?)? {
79+
Ok(KeyType::MlDsa87)
7180
} else {
7281
Err(pyo3::exceptions::PyTypeError::new_err(
73-
"Key must be an rsa, dsa, ec, ed25519, or ed448 private key.",
82+
"Key must be an rsa, dsa, ec, ed25519, ed448, ml-dsa-44, \
83+
ml-dsa-65, or ml-dsa-87 private key.",
7484
))
7585
}
7686
}
@@ -190,6 +200,24 @@ pub(crate) fn compute_signature_algorithm<'p>(
190200
"Algorithm must be None when signing via ed25519 or ed448",
191201
)),
192202

203+
(KeyType::MlDsa44, HashType::None) => Ok(common::AlgorithmIdentifier {
204+
oid: asn1::DefinedByMarker::marker(),
205+
params: common::AlgorithmParameters::MlDsa44,
206+
}),
207+
(KeyType::MlDsa65, HashType::None) => Ok(common::AlgorithmIdentifier {
208+
oid: asn1::DefinedByMarker::marker(),
209+
params: common::AlgorithmParameters::MlDsa65,
210+
}),
211+
(KeyType::MlDsa87, HashType::None) => Ok(common::AlgorithmIdentifier {
212+
oid: asn1::DefinedByMarker::marker(),
213+
params: common::AlgorithmParameters::MlDsa87,
214+
}),
215+
(KeyType::MlDsa44 | KeyType::MlDsa65 | KeyType::MlDsa87, _) => {
216+
Err(pyo3::exceptions::PyValueError::new_err(
217+
"Algorithm must be None when signing via ml-dsa-44, ml-dsa-65, or ml-dsa-87",
218+
))
219+
}
220+
193221
(KeyType::Ec, HashType::Sha224) => Ok(common::AlgorithmIdentifier {
194222
oid: asn1::DefinedByMarker::marker(),
195223
params: common::AlgorithmParameters::EcDsaWithSha224(None),
@@ -295,9 +323,11 @@ pub(crate) fn sign_data<'p>(
295323
let key_type = identify_key_type(py, private_key.clone())?;
296324

297325
let signature = match key_type {
298-
KeyType::Ed25519 | KeyType::Ed448 => {
299-
private_key.call_method1(pyo3::intern!(py, "sign"), (data,))?
300-
}
326+
KeyType::Ed25519
327+
| KeyType::Ed448
328+
| KeyType::MlDsa44
329+
| KeyType::MlDsa65
330+
| KeyType::MlDsa87 => private_key.call_method1(pyo3::intern!(py, "sign"), (data,))?,
301331
KeyType::Ec => {
302332
let ecdsa = types::ECDSA
303333
.get(py)?
@@ -338,7 +368,11 @@ pub(crate) fn verify_signature_with_signature_algorithm<'p>(
338368
identify_signature_algorithm_parameters(py, signature_algorithm)?;
339369
let py_signature_hash_algorithm = identify_signature_hash_algorithm(py, signature_algorithm)?;
340370
match key_type {
341-
KeyType::Ed25519 | KeyType::Ed448 => {
371+
KeyType::Ed25519
372+
| KeyType::Ed448
373+
| KeyType::MlDsa44
374+
| KeyType::MlDsa65
375+
| KeyType::MlDsa87 => {
342376
issuer_public_key.call_method1(pyo3::intern!(py, "verify"), (signature, data))?
343377
}
344378
KeyType::Ec => issuer_public_key.call_method1(
@@ -376,9 +410,16 @@ pub(crate) fn identify_public_key_type(
376410
Ok(KeyType::Ed25519)
377411
} else if public_key.is_instance(&types::ED448_PUBLIC_KEY.get(py)?)? {
378412
Ok(KeyType::Ed448)
413+
} else if public_key.is_instance(&types::MLDSA44_PUBLIC_KEY.get(py)?)? {
414+
Ok(KeyType::MlDsa44)
415+
} else if public_key.is_instance(&types::MLDSA65_PUBLIC_KEY.get(py)?)? {
416+
Ok(KeyType::MlDsa65)
417+
} else if public_key.is_instance(&types::MLDSA87_PUBLIC_KEY.get(py)?)? {
418+
Ok(KeyType::MlDsa87)
379419
} else {
380420
Err(pyo3::exceptions::PyTypeError::new_err(
381-
"Key must be an rsa, dsa, ec, ed25519, or ed448 public key.",
421+
"Key must be an rsa, dsa, ec, ed25519, ed448, ml-dsa-44, \
422+
ml-dsa-65, or ml-dsa-87 public key.",
382423
))
383424
}
384425
}
@@ -406,6 +447,9 @@ fn identify_key_type_for_algorithm_params(
406447
| common::AlgorithmParameters::EcDsaWithSha3_512 => Ok(KeyType::Ec),
407448
common::AlgorithmParameters::Ed25519 => Ok(KeyType::Ed25519),
408449
common::AlgorithmParameters::Ed448 => Ok(KeyType::Ed448),
450+
common::AlgorithmParameters::MlDsa44 => Ok(KeyType::MlDsa44),
451+
common::AlgorithmParameters::MlDsa65 => Ok(KeyType::MlDsa65),
452+
common::AlgorithmParameters::MlDsa87 => Ok(KeyType::MlDsa87),
409453
common::AlgorithmParameters::DsaWithSha224(..)
410454
| common::AlgorithmParameters::DsaWithSha256(..)
411455
| common::AlgorithmParameters::DsaWithSha384(..)
@@ -594,6 +638,9 @@ mod tests {
594638
(&common::AlgorithmParameters::EcDsaWithSha3_512, KeyType::Ec),
595639
(&common::AlgorithmParameters::Ed25519, KeyType::Ed25519),
596640
(&common::AlgorithmParameters::Ed448, KeyType::Ed448),
641+
(&common::AlgorithmParameters::MlDsa44, KeyType::MlDsa44),
642+
(&common::AlgorithmParameters::MlDsa65, KeyType::MlDsa65),
643+
(&common::AlgorithmParameters::MlDsa87, KeyType::MlDsa87),
597644
(
598645
&common::AlgorithmParameters::DsaWithSha224(None),
599646
KeyType::Dsa,

0 commit comments

Comments
 (0)