Skip to content

Commit 716cbe7

Browse files
alexclaude
andauthored
Add ML-KEM support when using BoringSSL (#14673)
* Add ML-KEM support when using BoringSSL BoringSSL supports ML-KEM-768 and ML-KEM-1024 via its EVP_PKEY API. This mirrors the existing ML-DSA BoringSSL support pattern, using EVP_pkey_ml_kem_768/1024 and EVP_PKEY_from_private_seed for key creation, and EVP_PKEY_encapsulate/decapsulate with the required _init calls for encapsulation/decapsulation. Key changes: - cryptography-openssl mlkem module: cfg-gated for both BoringSSL and AWS-LC with variant-specific code paths via cfg_if - Added is_mlkem_pkey_type() function (like mldsa) to replace single PKEY_ID matching, since BoringSSL uses per-variant NIDs - Key parsing (pkcs8/spki) enabled for BoringSSL - Python backend: mlkem_supported() now returns True for BoringSSL https://claude.ai/code/session_01CQzkjEGQfv1xVZ4UXy6Jqj * Reformat encapsulate/decapsulate _init calls for coverage Split the unsafe FFI call from the cvt() error check so that coverage tools can distinguish the call site from the error path. https://claude.ai/code/session_01CQzkjEGQfv1xVZ4UXy6Jqj * Move SAFETY comments inside cfg blocks for clippy The undocumented_unsafe_blocks lint requires SAFETY comments directly above the unsafe block, not separated by #[cfg] attrs. https://claude.ai/code/session_01CQzkjEGQfv1xVZ4UXy6Jqj * Run cargo fmt on _init call formatting https://claude.ai/code/session_01CQzkjEGQfv1xVZ4UXy6Jqj --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fc9e0d5 commit 716cbe7

8 files changed

Lines changed: 144 additions & 67 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,10 @@ def x448_supported(self) -> bool:
273273
)
274274

275275
def mlkem_supported(self) -> bool:
276-
return rust_openssl.CRYPTOGRAPHY_IS_AWSLC
276+
return (
277+
rust_openssl.CRYPTOGRAPHY_IS_AWSLC
278+
or rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL
279+
)
277280

278281
def mldsa_supported(self) -> bool:
279282
return (

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub struct PrivateKeyInfo<'a> {
2323
}
2424

2525
// RFC 9935 Section 6
26-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
26+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
2727
#[derive(asn1::Asn1Read, asn1::Asn1Write)]
2828
pub enum MlKemPrivateKey {
2929
#[implicit(0)]
@@ -43,7 +43,8 @@ pub enum MlDsaPrivateKey {
4343
/// AWS-LC's `raw_private_key()` returns the 2400-byte expanded key, not the seed.
4444
/// Since AWS-LC 1.72.0, `private_key_to_pkcs8()` produces RFC 9935 seed-format
4545
/// PKCS#8 when the key was created from a seed, so we round-trip through that.
46-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
46+
/// BoringSSL's private key serialization also emits RFC 9935 seed-format PKCS#8.
47+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
4748
pub fn mlkem_seed_from_pkey(
4849
pkey: &openssl::pkey::PKeyRef<openssl::pkey::Private>,
4950
) -> Result<MlKemPrivateKey, openssl::error::ErrorStack> {
@@ -154,7 +155,7 @@ pub fn parse_private_key(data: &[u8]) -> KeyParsingResult<ParsedPrivateKey> {
154155
Ok(ParsedPrivateKey::Pkey(pkey))
155156
}
156157

157-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
158+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
158159
AlgorithmParameters::MlKem768 => {
159160
let MlKemPrivateKey::Seed(seed) = asn1::parse_single::<MlKemPrivateKey>(k.private_key)?;
160161
let pkey = cryptography_openssl::mlkem::new_raw_private_key(
@@ -164,7 +165,7 @@ pub fn parse_private_key(data: &[u8]) -> KeyParsingResult<ParsedPrivateKey> {
164165
Ok(ParsedPrivateKey::Pkey(pkey))
165166
}
166167

167-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
168+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
168169
AlgorithmParameters::MlKem1024 => {
169170
let MlKemPrivateKey::Seed(seed) = asn1::parse_single::<MlKemPrivateKey>(k.private_key)?;
170171
let pkey = cryptography_openssl::mlkem::new_raw_private_key(
@@ -541,8 +542,8 @@ pub fn serialize_private_key(key: &ParsedPrivateKey) -> crate::KeySerializationR
541542

542543
(params, private_key_der)
543544
}
544-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
545-
cryptography_openssl::mlkem::PKEY_ID => {
545+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
546+
id if cryptography_openssl::mlkem::is_mlkem_pkey_type(id) => {
546547
let private_key_der = asn1::write_single(&mlkem_seed_from_pkey(pkey)?)?;
547548
let params = match cryptography_openssl::mlkem::MlKemVariant::from_pkey(pkey) {
548549
cryptography_openssl::mlkem::MlKemVariant::MlKem768 => {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,15 @@ pub fn parse_public_key(data: &[u8]) -> KeyParsingResult<ParsedPublicKey> {
109109

110110
Ok(ParsedPublicKey::Pkey(openssl::pkey::PKey::from_dh(dh)?))
111111
}
112-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
112+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
113113
AlgorithmParameters::MlKem768 => Ok(ParsedPublicKey::Pkey(
114114
cryptography_openssl::mlkem::new_raw_public_key(
115115
cryptography_openssl::mlkem::MlKemVariant::MlKem768,
116116
k.subject_public_key.as_bytes(),
117117
)
118118
.map_err(|_| KeyParsingError::InvalidKey)?,
119119
)),
120-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
120+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
121121
AlgorithmParameters::MlKem1024 => Ok(ParsedPublicKey::Pkey(
122122
cryptography_openssl::mlkem::new_raw_public_key(
123123
cryptography_openssl::mlkem::MlKemVariant::MlKem1024,
@@ -264,8 +264,8 @@ pub fn serialize_public_key(
264264

265265
(params, pub_key_der)
266266
}
267-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
268-
cryptography_openssl::mlkem::PKEY_ID => {
267+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
268+
id if cryptography_openssl::mlkem::is_mlkem_pkey_type(id) => {
269269
let raw_bytes = pkey.raw_public_key()?;
270270
let params = match cryptography_openssl::mlkem::MlKemVariant::from_pkey(pkey) {
271271
cryptography_openssl::mlkem::MlKemVariant::MlKem768 => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub mod fips;
1111
pub mod hmac;
1212
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
1313
pub mod mldsa;
14-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
14+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
1515
pub mod mlkem;
1616
#[cfg(any(
1717
CRYPTOGRAPHY_IS_BORINGSSL,

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

Lines changed: 122 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,33 @@
44

55
use foreign_types_shared::ForeignType;
66
use openssl_sys as ffi;
7+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
78
use std::os::raw::c_int;
89

910
use crate::{cvt, cvt_p, OpenSSLResult};
1011

12+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
1113
pub const PKEY_ID: openssl::pkey::Id = openssl::pkey::Id::from_raw(ffi::NID_kem);
1214

15+
pub fn is_mlkem_pkey_type(id: openssl::pkey::Id) -> bool {
16+
cfg_if::cfg_if! {
17+
if #[cfg(CRYPTOGRAPHY_IS_BORINGSSL)] {
18+
let raw = id.as_raw();
19+
raw == ffi::NID_ML_KEM_768 || raw == ffi::NID_ML_KEM_1024
20+
} else if #[cfg(CRYPTOGRAPHY_IS_AWSLC)] {
21+
id == PKEY_ID
22+
}
23+
}
24+
}
25+
1326
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1427
pub enum MlKemVariant {
1528
MlKem768,
1629
MlKem1024,
1730
}
1831

1932
impl MlKemVariant {
33+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
2034
pub fn nid(self) -> c_int {
2135
match self {
2236
MlKemVariant::MlKem768 => ffi::NID_MLKEM768,
@@ -27,20 +41,44 @@ impl MlKemVariant {
2741
pub fn from_pkey<T: openssl::pkey::HasPublic>(
2842
pkey: &openssl::pkey::PKeyRef<T>,
2943
) -> MlKemVariant {
30-
// AWS-LC is missing the equivalent `EVP_PKEY_pqdsa_get_type`, so we
31-
// are using the key size as a discriminator to find the variant.
32-
let len = pkey
33-
.raw_public_key()
34-
.expect("valid ML-KEM public key")
35-
.len();
36-
match len {
37-
1184 => MlKemVariant::MlKem768,
38-
1568 => MlKemVariant::MlKem1024,
39-
_ => panic!("Unsupported ML-KEM variant"),
44+
cfg_if::cfg_if! {
45+
if #[cfg(CRYPTOGRAPHY_IS_BORINGSSL)] {
46+
match pkey.id().as_raw() {
47+
ffi::NID_ML_KEM_768 => MlKemVariant::MlKem768,
48+
ffi::NID_ML_KEM_1024 => MlKemVariant::MlKem1024,
49+
_ => panic!("Unsupported ML-KEM variant"),
50+
}
51+
} else if #[cfg(CRYPTOGRAPHY_IS_AWSLC)] {
52+
// AWS-LC is missing the equivalent `EVP_PKEY_pqdsa_get_type`,
53+
// so we are using the key size as a discriminator to find the
54+
// variant.
55+
let len = pkey
56+
.raw_public_key()
57+
.expect("valid ML-KEM public key")
58+
.len();
59+
match len {
60+
1184 => MlKemVariant::MlKem768,
61+
1568 => MlKemVariant::MlKem1024,
62+
_ => panic!("Unsupported ML-KEM variant"),
63+
}
64+
}
65+
}
66+
}
67+
}
68+
69+
#[cfg(CRYPTOGRAPHY_IS_BORINGSSL)]
70+
fn evp_pkey_alg(variant: MlKemVariant) -> *const ffi::EVP_PKEY_ALG {
71+
// SAFETY: These functions return static, non-null pointers to the
72+
// EVP_PKEY_ALG for each ML-KEM variant.
73+
unsafe {
74+
match variant {
75+
MlKemVariant::MlKem768 => ffi::EVP_pkey_ml_kem_768(),
76+
MlKemVariant::MlKem1024 => ffi::EVP_pkey_ml_kem_1024(),
4077
}
4178
}
4279
}
4380

81+
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
4482
extern "C" {
4583
// Manually declared because this function is in an experimental header
4684
// in AWS-LC (April 2026).
@@ -57,49 +95,74 @@ pub fn new_raw_private_key(
5795
variant: MlKemVariant,
5896
seed: &[u8],
5997
) -> OpenSSLResult<openssl::pkey::PKey<openssl::pkey::Private>> {
60-
let ctx = openssl::pkey_ctx::PkeyCtx::new_id(PKEY_ID)?;
61-
// SAFETY: ctx is a valid EVP_PKEY_CTX for KEM.
62-
unsafe {
63-
cvt(ffi::EVP_PKEY_CTX_kem_set_params(
64-
ctx.as_ptr(),
65-
variant.nid(),
66-
))?
67-
};
68-
// SAFETY: ctx is a valid EVP_PKEY_CTX with KEM params set.
69-
unsafe { cvt(ffi::EVP_PKEY_keygen_init(ctx.as_ptr()))? };
70-
71-
let mut pkey: *mut ffi::EVP_PKEY = std::ptr::null_mut();
72-
let mut seed_len = seed.len();
73-
// SAFETY: ctx is initialized for keygen, seed points to valid memory.
74-
unsafe {
75-
cvt(EVP_PKEY_keygen_deterministic(
76-
ctx.as_ptr(),
77-
&mut pkey,
78-
seed.as_ptr(),
79-
&mut seed_len,
80-
))?;
98+
cfg_if::cfg_if! {
99+
if #[cfg(CRYPTOGRAPHY_IS_BORINGSSL)] {
100+
// SAFETY: EVP_PKEY_from_private_seed creates a new EVP_PKEY from
101+
// the seed. evp_pkey_alg returns a valid algorithm pointer.
102+
unsafe {
103+
let pkey = cvt_p(ffi::EVP_PKEY_from_private_seed(
104+
evp_pkey_alg(variant),
105+
seed.as_ptr(),
106+
seed.len(),
107+
))?;
108+
Ok(openssl::pkey::PKey::from_ptr(pkey))
109+
}
110+
} else if #[cfg(CRYPTOGRAPHY_IS_AWSLC)] {
111+
let ctx = openssl::pkey_ctx::PkeyCtx::new_id(PKEY_ID)?;
112+
// SAFETY: ctx is a valid EVP_PKEY_CTX for KEM.
113+
unsafe {
114+
cvt(ffi::EVP_PKEY_CTX_kem_set_params(
115+
ctx.as_ptr(),
116+
variant.nid(),
117+
))?
118+
};
119+
// SAFETY: ctx is a valid EVP_PKEY_CTX with KEM params set.
120+
unsafe { cvt(ffi::EVP_PKEY_keygen_init(ctx.as_ptr()))? };
121+
122+
let mut pkey: *mut ffi::EVP_PKEY = std::ptr::null_mut();
123+
let mut seed_len = seed.len();
124+
// SAFETY: ctx is initialized for keygen, seed points to valid memory.
125+
unsafe {
126+
cvt(EVP_PKEY_keygen_deterministic(
127+
ctx.as_ptr(),
128+
&mut pkey,
129+
seed.as_ptr(),
130+
&mut seed_len,
131+
))?;
132+
}
133+
assert_eq!(seed_len, 64);
134+
// SAFETY: EVP_PKEY_keygen_deterministic succeeded, pkey is valid.
135+
let pkey = unsafe { openssl::pkey::PKey::from_ptr(pkey) };
136+
Ok(pkey)
137+
}
81138
}
82-
let expected_seed_len = match variant {
83-
MlKemVariant::MlKem768 | MlKemVariant::MlKem1024 => 64,
84-
};
85-
assert_eq!(seed_len, expected_seed_len);
86-
// SAFETY: EVP_PKEY_keygen_deterministic succeeded, pkey is valid.
87-
let pkey = unsafe { openssl::pkey::PKey::from_ptr(pkey) };
88-
Ok(pkey)
89139
}
90140

91141
pub fn new_raw_public_key(
92142
variant: MlKemVariant,
93143
data: &[u8],
94144
) -> OpenSSLResult<openssl::pkey::PKey<openssl::pkey::Public>> {
95-
// SAFETY: data points to valid memory of the given length.
96-
unsafe {
97-
let pkey = cvt_p(ffi::EVP_PKEY_kem_new_raw_public_key(
98-
variant.nid(),
99-
data.as_ptr(),
100-
data.len(),
101-
))?;
102-
Ok(openssl::pkey::PKey::from_ptr(pkey))
145+
cfg_if::cfg_if! {
146+
if #[cfg(CRYPTOGRAPHY_IS_BORINGSSL)] {
147+
let nid = match variant {
148+
MlKemVariant::MlKem768 => ffi::NID_ML_KEM_768,
149+
MlKemVariant::MlKem1024 => ffi::NID_ML_KEM_1024,
150+
};
151+
openssl::pkey::PKey::public_key_from_raw_bytes(
152+
data,
153+
openssl::pkey::Id::from_raw(nid),
154+
)
155+
} else if #[cfg(CRYPTOGRAPHY_IS_AWSLC)] {
156+
// SAFETY: data points to valid memory of the given length.
157+
unsafe {
158+
let pkey = cvt_p(ffi::EVP_PKEY_kem_new_raw_public_key(
159+
variant.nid(),
160+
data.as_ptr(),
161+
data.len(),
162+
))?;
163+
Ok(openssl::pkey::PKey::from_ptr(pkey))
164+
}
165+
}
103166
}
104167
}
105168

@@ -111,6 +174,12 @@ pub fn encapsulate(
111174
MlKemVariant::MlKem1024 => (1568, 32),
112175
};
113176
let ctx = openssl::pkey_ctx::PkeyCtx::new(pkey)?;
177+
#[cfg(CRYPTOGRAPHY_IS_BORINGSSL)]
178+
{
179+
// SAFETY: ctx is a valid EVP_PKEY_CTX for the KEM operation.
180+
let res = unsafe { ffi::EVP_PKEY_encapsulate_init(ctx.as_ptr(), std::ptr::null()) };
181+
cvt(res)?;
182+
}
114183

115184
let mut ciphertext = vec![0u8; ct_bytes];
116185
let mut shared_secret = vec![0u8; ss_bytes];
@@ -136,10 +205,14 @@ pub fn decapsulate(
136205
ciphertext: &[u8],
137206
) -> OpenSSLResult<Vec<u8>> {
138207
let ctx = openssl::pkey_ctx::PkeyCtx::new(pkey)?;
208+
#[cfg(CRYPTOGRAPHY_IS_BORINGSSL)]
209+
{
210+
// SAFETY: ctx is a valid EVP_PKEY_CTX for the KEM operation.
211+
let res = unsafe { ffi::EVP_PKEY_decapsulate_init(ctx.as_ptr(), std::ptr::null()) };
212+
cvt(res)?;
213+
}
139214

140-
let ss_bytes: usize = match MlKemVariant::from_pkey(pkey) {
141-
MlKemVariant::MlKem768 | MlKemVariant::MlKem1024 => 32,
142-
};
215+
let ss_bytes: usize = 32;
143216
let mut shared_secret = vec![0u8; ss_bytes];
144217
let mut ss_len = ss_bytes;
145218
// SAFETY: ctx is a valid EVP_PKEY_CTX, buffers are correctly sized.

src/rust/src/backend/keys.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ fn private_key_from_pkey<'p>(
180180
openssl::pkey::Id::DHX => Ok(crate::backend::dh::private_key_from_pkey(pkey)
181181
.into_pyobject(py)?
182182
.into_any()),
183-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
184-
cryptography_openssl::mlkem::PKEY_ID => {
183+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
184+
id if cryptography_openssl::mlkem::is_mlkem_pkey_type(id) => {
185185
match cryptography_openssl::mlkem::MlKemVariant::from_pkey(pkey) {
186186
cryptography_openssl::mlkem::MlKemVariant::MlKem768 => {
187187
Ok(crate::backend::mlkem::mlkem768_private_key_from_pkey(pkey)
@@ -370,8 +370,8 @@ fn public_key_from_pkey<'p>(
370370
openssl::pkey::Id::DHX => Ok(crate::backend::dh::public_key_from_pkey(pkey)
371371
.into_pyobject(py)?
372372
.into_any()),
373-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
374-
cryptography_openssl::mlkem::PKEY_ID => {
373+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
374+
id if cryptography_openssl::mlkem::is_mlkem_pkey_type(id) => {
375375
match cryptography_openssl::mlkem::MlKemVariant::from_pkey(pkey) {
376376
cryptography_openssl::mlkem::MlKemVariant::MlKem768 => {
377377
Ok(crate::backend::mlkem::mlkem768_public_key_from_pkey(pkey)

src/rust/src/backend/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub(crate) mod kdf;
2323
pub(crate) mod keys;
2424
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
2525
pub(crate) mod mldsa;
26-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
26+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
2727
pub(crate) mod mlkem;
2828
pub(crate) mod poly1305;
2929
pub(crate) mod rand;

src/rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ mod _rust {
245245
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
246246
#[pymodule_export]
247247
use crate::backend::mldsa::mldsa;
248-
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
248+
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
249249
#[pymodule_export]
250250
use crate::backend::mlkem::mlkem;
251251
#[pymodule_export]

0 commit comments

Comments
 (0)