Skip to content

Commit f630c29

Browse files
authored
MLDSA65 support for AWS-LC (#14404)
* MLDSA65 support for AWS-LC * Improve coverage * Initial review * First round of review * Clean tests * Revert spurious formatting * Incorporate review * Rename from mldsa65 to mldsa * Improve serialization/deserialization * Fix coverage * Use ASN1 struct to parse the private key * Use Enum instead of Struct * Inline function * Change match arm * Remove duplicate comment * Change unimplemented by assert * More review fix * Fix Wycheproof vectors handling * Add NO-COVERAGE marker * Remove vectors * Review comments * Remove markers * Additional round of review
1 parent 22eb61b commit f630c29

File tree

17 files changed

+1098
-0
lines changed

17 files changed

+1098
-0
lines changed

src/cryptography/hazmat/backends/openssl/backend.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ def x448_supported(self) -> bool:
272272
and not rust_openssl.CRYPTOGRAPHY_IS_AWSLC
273273
)
274274

275+
def mldsa_supported(self) -> bool:
276+
return rust_openssl.CRYPTOGRAPHY_IS_AWSLC
277+
275278
def ed25519_supported(self) -> bool:
276279
return not self._fips_enabled
277280

src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ from cryptography.hazmat.bindings._rust.openssl import (
1818
hpke,
1919
kdf,
2020
keys,
21+
mldsa,
2122
poly1305,
2223
rsa,
2324
x448,
@@ -38,6 +39,7 @@ __all__ = [
3839
"hpke",
3940
"kdf",
4041
"keys",
42+
"mldsa",
4143
"openssl_version",
4244
"openssl_version_text",
4345
"poly1305",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.
4+
5+
from cryptography.hazmat.primitives.asymmetric import mldsa
6+
from cryptography.utils import Buffer
7+
8+
class MlDsa65PrivateKey: ...
9+
class MlDsa65PublicKey: ...
10+
11+
def generate_key() -> mldsa.MlDsa65PrivateKey: ...
12+
def from_public_bytes(data: bytes) -> mldsa.MlDsa65PublicKey: ...
13+
def from_seed_bytes(data: Buffer) -> mldsa.MlDsa65PrivateKey: ...
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.
4+
5+
from __future__ import annotations
6+
7+
import abc
8+
9+
from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
10+
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
11+
from cryptography.hazmat.primitives import _serialization
12+
from cryptography.utils import Buffer
13+
14+
15+
class MlDsa65PublicKey(metaclass=abc.ABCMeta):
16+
@classmethod
17+
def from_public_bytes(cls, data: bytes) -> MlDsa65PublicKey:
18+
from cryptography.hazmat.backends.openssl.backend import backend
19+
20+
if not backend.mldsa_supported():
21+
raise UnsupportedAlgorithm(
22+
"ML-DSA-65 is not supported by this backend.",
23+
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
24+
)
25+
26+
return rust_openssl.mldsa.from_public_bytes(data)
27+
28+
@abc.abstractmethod
29+
def public_bytes(
30+
self,
31+
encoding: _serialization.Encoding,
32+
format: _serialization.PublicFormat,
33+
) -> bytes:
34+
"""
35+
The serialized bytes of the public key.
36+
"""
37+
38+
@abc.abstractmethod
39+
def public_bytes_raw(self) -> bytes:
40+
"""
41+
The raw bytes of the public key.
42+
Equivalent to public_bytes(Raw, Raw).
43+
44+
The public key is 1,952 bytes for MLDSA-65.
45+
"""
46+
47+
@abc.abstractmethod
48+
def verify(
49+
self,
50+
signature: Buffer,
51+
data: Buffer,
52+
context: Buffer | None = None,
53+
) -> None:
54+
"""
55+
Verify the signature.
56+
"""
57+
58+
@abc.abstractmethod
59+
def __eq__(self, other: object) -> bool:
60+
"""
61+
Checks equality.
62+
"""
63+
64+
@abc.abstractmethod
65+
def __copy__(self) -> MlDsa65PublicKey:
66+
"""
67+
Returns a copy.
68+
"""
69+
70+
@abc.abstractmethod
71+
def __deepcopy__(self, memo: dict) -> MlDsa65PublicKey:
72+
"""
73+
Returns a deep copy.
74+
"""
75+
76+
77+
if hasattr(rust_openssl, "mldsa"):
78+
MlDsa65PublicKey.register(rust_openssl.mldsa.MlDsa65PublicKey)
79+
80+
81+
class MlDsa65PrivateKey(metaclass=abc.ABCMeta):
82+
@classmethod
83+
def generate(cls) -> MlDsa65PrivateKey:
84+
from cryptography.hazmat.backends.openssl.backend import backend
85+
86+
if not backend.mldsa_supported():
87+
raise UnsupportedAlgorithm(
88+
"ML-DSA-65 is not supported by this backend.",
89+
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
90+
)
91+
92+
return rust_openssl.mldsa.generate_key()
93+
94+
@classmethod
95+
def from_seed_bytes(cls, data: Buffer) -> MlDsa65PrivateKey:
96+
from cryptography.hazmat.backends.openssl.backend import backend
97+
98+
if not backend.mldsa_supported():
99+
raise UnsupportedAlgorithm(
100+
"ML-DSA-65 is not supported by this backend.",
101+
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM,
102+
)
103+
104+
return rust_openssl.mldsa.from_seed_bytes(data)
105+
106+
@abc.abstractmethod
107+
def public_key(self) -> MlDsa65PublicKey:
108+
"""
109+
The MlDsa65PublicKey derived from the private key.
110+
"""
111+
112+
@abc.abstractmethod
113+
def private_bytes(
114+
self,
115+
encoding: _serialization.Encoding,
116+
format: _serialization.PrivateFormat,
117+
encryption_algorithm: _serialization.KeySerializationEncryption,
118+
) -> bytes:
119+
"""
120+
The serialized bytes of the private key.
121+
122+
This method only returns the serialization of the seed form of the
123+
private key, never the expanded one.
124+
"""
125+
126+
@abc.abstractmethod
127+
def private_bytes_raw(self) -> bytes:
128+
"""
129+
The raw bytes of the private key.
130+
Equivalent to private_bytes(Raw, Raw, NoEncryption()).
131+
132+
This method only returns the seed form of the private key (32 bytes).
133+
"""
134+
135+
@abc.abstractmethod
136+
def sign(self, data: Buffer, context: Buffer | None = None) -> bytes:
137+
"""
138+
Signs the data.
139+
"""
140+
141+
@abc.abstractmethod
142+
def __copy__(self) -> MlDsa65PrivateKey:
143+
"""
144+
Returns a copy.
145+
"""
146+
147+
@abc.abstractmethod
148+
def __deepcopy__(self, memo: dict) -> MlDsa65PrivateKey:
149+
"""
150+
Returns a deep copy.
151+
"""
152+
153+
154+
if hasattr(rust_openssl, "mldsa"):
155+
MlDsa65PrivateKey.register(rust_openssl.mldsa.MlDsa65PrivateKey)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ec,
1414
ed448,
1515
ed25519,
16+
mldsa,
1617
rsa,
1718
x448,
1819
x25519,
@@ -26,6 +27,7 @@
2627
ec.EllipticCurvePublicKey,
2728
ed25519.Ed25519PublicKey,
2829
ed448.Ed448PublicKey,
30+
mldsa.MlDsa65PublicKey,
2931
x25519.X25519PublicKey,
3032
x448.X448PublicKey,
3133
]
@@ -42,6 +44,7 @@
4244
dh.DHPrivateKey,
4345
ed25519.Ed25519PrivateKey,
4446
ed448.Ed448PrivateKey,
47+
mldsa.MlDsa65PrivateKey,
4548
rsa.RSAPrivateKey,
4649
dsa.DSAPrivateKey,
4750
ec.EllipticCurvePrivateKey,

src/rust/cryptography-key-parsing/src/pkcs8.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,28 @@ pub struct PrivateKeyInfo<'a> {
2222
pub attributes: Option<Attributes<'a>>,
2323
}
2424

25+
// RFC 9881 Section 6.5
26+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
27+
#[derive(asn1::Asn1Read, asn1::Asn1Write)]
28+
pub enum MlDsaPrivateKey {
29+
#[implicit(0)]
30+
Seed([u8; 32]),
31+
}
32+
33+
/// Extract the 32-byte ML-DSA-65 seed from a private key.
34+
///
35+
/// AWS-LC's `raw_private_key()` returns the expanded key, not the seed.
36+
/// This function round-trips through the native PKCS#8 encoding to extract it.
37+
/// https://github.com/aws/aws-lc/issues/3072
38+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
39+
pub fn mldsa_seed_from_pkey(
40+
pkey: &openssl::pkey::PKeyRef<openssl::pkey::Private>,
41+
) -> Result<MlDsaPrivateKey, openssl::error::ErrorStack> {
42+
let pkcs8_der = pkey.private_key_to_pkcs8()?;
43+
let pki = asn1::parse_single::<PrivateKeyInfo<'_>>(&pkcs8_der).unwrap();
44+
Ok(asn1::parse_single::<MlDsaPrivateKey>(pki.private_key).unwrap())
45+
}
46+
2547
pub fn parse_private_key(
2648
data: &[u8],
2749
) -> KeyParsingResult<openssl::pkey::PKey<openssl::pkey::Private>> {
@@ -108,6 +130,12 @@ pub fn parse_private_key(
108130
)?)
109131
}
110132

133+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
134+
AlgorithmParameters::MlDsa65 => {
135+
let MlDsaPrivateKey::Seed(seed) = asn1::parse_single::<MlDsaPrivateKey>(k.private_key)?;
136+
Ok(cryptography_openssl::mldsa::new_raw_private_key(&seed)?)
137+
}
138+
111139
_ => Err(KeyParsingError::UnsupportedKeyType(
112140
k.algorithm.oid().clone(),
113141
)),
@@ -443,6 +471,11 @@ pub fn serialize_private_key(
443471

444472
(params, private_key_der)
445473
}
474+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
475+
cryptography_openssl::mldsa::PKEY_ID => {
476+
let private_key_der = asn1::write_single(&mldsa_seed_from_pkey(pkey)?)?;
477+
(AlgorithmParameters::MlDsa65, private_key_der)
478+
}
446479
_ => {
447480
unimplemented!("Unknown key type");
448481
}

src/rust/cryptography-key-parsing/src/spki.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ pub fn parse_public_key(
100100

101101
Ok(openssl::pkey::PKey::from_dh(dh)?)
102102
}
103+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
104+
AlgorithmParameters::MlDsa65 => Ok(cryptography_openssl::mldsa::new_raw_public_key(
105+
k.subject_public_key.as_bytes(),
106+
)
107+
.map_err(|_| KeyParsingError::InvalidKey)?),
108+
103109
_ => Err(KeyParsingError::UnsupportedKeyType(
104110
k.algorithm.oid().clone(),
105111
)),
@@ -214,6 +220,15 @@ pub fn serialize_public_key(
214220

215221
(params, pub_key_der)
216222
}
223+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
224+
cryptography_openssl::mldsa::PKEY_ID => {
225+
let raw_bytes = pkey.raw_public_key()?;
226+
assert_eq!(
227+
raw_bytes.len(),
228+
cryptography_openssl::mldsa::MLDSA65_PUBLIC_KEY_BYTES
229+
);
230+
(AlgorithmParameters::MlDsa65, raw_bytes)
231+
}
217232
_ => {
218233
unimplemented!("Unknown key type");
219234
}

src/rust/cryptography-openssl/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub mod aead;
99
pub mod cmac;
1010
pub mod fips;
1111
pub mod hmac;
12+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
13+
pub mod mldsa;
1214
#[cfg(any(
1315
CRYPTOGRAPHY_IS_BORINGSSL,
1416
CRYPTOGRAPHY_IS_LIBRESSL,

0 commit comments

Comments
 (0)