Skip to content

Commit a6cabfa

Browse files
alexclaude
andauthored
Add ChaCha20Poly1305 support to HPKE implementation (#14393)
Add CHACHA20_POLY1305 as an AEAD option for HPKE (RFC 9180 aead_id 0x0003), alongside the existing AES_128_GCM support. Changes: - Add CHACHA20_POLY1305 variant to the HPKE AEAD enum with parameters (Nk=32, Nn=12, Nt=16) - Make Suite store the selected AEAD and dynamically dispatch to the correct cipher for encrypt/decrypt operations - Make key_schedule return PyBytes directly to avoid unnecessary allocations when passing to AEAD constructors - Make ChaCha20Poly1305 struct and its encrypt/decrypt methods pub(crate) so they can be used from the HPKE module - Update tests to cover ChaCha20Poly1305 roundtrips and RFC 9180 test vector validation - Update type stubs and documentation https://claude.ai/code/session_019vzyAZNnzXmtyAnkc9BHZp Co-authored-by: Claude <noreply@anthropic.com>
1 parent 92a376f commit a6cabfa

5 files changed

Lines changed: 120 additions & 34 deletions

File tree

docs/hazmat/primitives/hpke.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Mechanism (KEM), a Key Derivation Function (KDF), and an Authenticated
1010
Encryption with Associated Data (AEAD) scheme. It is defined in :rfc:`9180`.
1111

1212
This implementation supports Base mode with DHKEM(X25519, HKDF-SHA256),
13-
HKDF-SHA256, and AES-128-GCM.
13+
HKDF-SHA256, and either AES-128-GCM or ChaCha20Poly1305.
1414

1515
HPKE provides authenticated encryption: the recipient can be certain that the
1616
message was encrypted by someone who knows the recipient's public key, but
@@ -99,3 +99,7 @@ specifying auxiliary authenticated information.
9999
.. attribute:: AES_128_GCM
100100

101101
AES-128-GCM
102+
103+
.. attribute:: CHACHA20_POLY1305
104+
105+
ChaCha20Poly1305

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class KDF:
1313

1414
class AEAD:
1515
AES_128_GCM: AEAD
16+
CHACHA20_POLY1305: AEAD
1617

1718
class Suite:
1819
def __init__(self, kem: KEM, kdf: KDF, aead: AEAD) -> None: ...

src/rust/src/backend/aead.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ impl EvpAead {
419419
}
420420

421421
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.aead")]
422-
struct ChaCha20Poly1305 {
422+
pub(crate) struct ChaCha20Poly1305 {
423423
#[cfg(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))]
424424
ctx: EvpAead,
425425
#[cfg(any(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER, CRYPTOGRAPHY_IS_LIBRESSL))]
@@ -436,7 +436,10 @@ struct ChaCha20Poly1305 {
436436
#[pyo3::pymethods]
437437
impl ChaCha20Poly1305 {
438438
#[new]
439-
fn new(py: pyo3::Python<'_>, key: pyo3::Py<pyo3::PyAny>) -> CryptographyResult<Self> {
439+
pub(crate) fn new(
440+
py: pyo3::Python<'_>,
441+
key: pyo3::Py<pyo3::PyAny>,
442+
) -> CryptographyResult<Self> {
440443
let key_buf = key.extract::<CffiBuf<'_>>(py)?;
441444
if key_buf.as_bytes().len() != 32 {
442445
return Err(CryptographyError::from(
@@ -495,7 +498,7 @@ impl ChaCha20Poly1305 {
495498
}
496499

497500
#[pyo3(signature = (nonce, data, associated_data))]
498-
fn encrypt<'p>(
501+
pub(crate) fn encrypt<'p>(
499502
&self,
500503
py: pyo3::Python<'p>,
501504
nonce: CffiBuf<'_>,
@@ -553,7 +556,7 @@ impl ChaCha20Poly1305 {
553556
}
554557

555558
#[pyo3(signature = (nonce, data, associated_data))]
556-
fn decrypt<'p>(
559+
pub(crate) fn decrypt<'p>(
557560
&self,
558561
py: pyo3::Python<'p>,
559562
nonce: CffiBuf<'_>,

src/rust/src/backend/hpke.rs

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use pyo3::types::{PyAnyMethods, PyBytesMethods};
66

7-
use crate::backend::aead::AesGcm;
7+
use crate::backend::aead::{AesGcm, ChaCha20Poly1305};
88
use crate::backend::kdf::{hkdf_extract, HkdfExpand};
99
use crate::backend::x25519;
1010
use crate::buf::CffiBuf;
@@ -29,6 +29,11 @@ mod aead_params {
2929
pub const AES_128_GCM_NK: usize = 16;
3030
pub const AES_128_GCM_NN: usize = 12;
3131
pub const AES_128_GCM_NT: usize = 16;
32+
33+
pub const CHACHA20_POLY1305_ID: u16 = 0x0003;
34+
pub const CHACHA20_POLY1305_NK: usize = 32;
35+
pub const CHACHA20_POLY1305_NN: usize = 12;
36+
pub const CHACHA20_POLY1305_NT: usize = 16;
3237
}
3338

3439
#[allow(clippy::upper_case_acronyms)]
@@ -70,10 +75,42 @@ pub(crate) enum KDF {
7075
#[derive(Clone, PartialEq, Eq, Hash)]
7176
pub(crate) enum AEAD {
7277
AES_128_GCM,
78+
CHACHA20_POLY1305,
79+
}
80+
81+
impl AEAD {
82+
fn id(&self) -> u16 {
83+
match self {
84+
AEAD::AES_128_GCM => aead_params::AES_128_GCM_ID,
85+
AEAD::CHACHA20_POLY1305 => aead_params::CHACHA20_POLY1305_ID,
86+
}
87+
}
88+
89+
fn key_length(&self) -> usize {
90+
match self {
91+
AEAD::AES_128_GCM => aead_params::AES_128_GCM_NK,
92+
AEAD::CHACHA20_POLY1305 => aead_params::CHACHA20_POLY1305_NK,
93+
}
94+
}
95+
96+
fn nonce_length(&self) -> usize {
97+
match self {
98+
AEAD::AES_128_GCM => aead_params::AES_128_GCM_NN,
99+
AEAD::CHACHA20_POLY1305 => aead_params::CHACHA20_POLY1305_NN,
100+
}
101+
}
102+
103+
fn tag_length(&self) -> usize {
104+
match self {
105+
AEAD::AES_128_GCM => aead_params::AES_128_GCM_NT,
106+
AEAD::CHACHA20_POLY1305 => aead_params::CHACHA20_POLY1305_NT,
107+
}
108+
}
73109
}
74110

75111
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.hpke")]
76112
pub(crate) struct Suite {
113+
aead: AEAD,
77114
kem_suite_id: [u8; 5],
78115
hpke_suite_id: [u8; 10],
79116
}
@@ -235,6 +272,50 @@ impl Suite {
235272
self.hkdf_expand(py, prk, &labeled_info, length)
236273
}
237274

275+
fn aead_encrypt<'p>(
276+
&self,
277+
py: pyo3::Python<'p>,
278+
key: &pyo3::Bound<'_, pyo3::types::PyBytes>,
279+
nonce: &pyo3::Bound<'_, pyo3::types::PyBytes>,
280+
plaintext: CffiBuf<'_>,
281+
aad: Option<CffiBuf<'_>>,
282+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
283+
let key_obj = key.clone().unbind().into_any();
284+
let nonce_buf = CffiBuf::from_bytes(py, nonce.as_bytes());
285+
match &self.aead {
286+
AEAD::AES_128_GCM => {
287+
let cipher = AesGcm::new(py, key_obj)?;
288+
cipher.encrypt(py, nonce_buf, plaintext, aad)
289+
}
290+
AEAD::CHACHA20_POLY1305 => {
291+
let cipher = ChaCha20Poly1305::new(py, key_obj)?;
292+
cipher.encrypt(py, nonce_buf, plaintext, aad)
293+
}
294+
}
295+
}
296+
297+
fn aead_decrypt<'p>(
298+
&self,
299+
py: pyo3::Python<'p>,
300+
key: &pyo3::Bound<'_, pyo3::types::PyBytes>,
301+
nonce: &pyo3::Bound<'_, pyo3::types::PyBytes>,
302+
ciphertext: &[u8],
303+
aad: Option<CffiBuf<'_>>,
304+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
305+
let key_obj = key.clone().unbind().into_any();
306+
let nonce_buf = CffiBuf::from_bytes(py, nonce.as_bytes());
307+
match &self.aead {
308+
AEAD::AES_128_GCM => {
309+
let cipher = AesGcm::new(py, key_obj)?;
310+
cipher.decrypt(py, nonce_buf, CffiBuf::from_bytes(py, ciphertext), aad)
311+
}
312+
AEAD::CHACHA20_POLY1305 => {
313+
let cipher = ChaCha20Poly1305::new(py, key_obj)?;
314+
cipher.decrypt(py, nonce_buf, CffiBuf::from_bytes(py, ciphertext), aad)
315+
}
316+
}
317+
}
318+
238319
fn encrypt_inner<'p>(
239320
&self,
240321
py: pyo3::Python<'p>,
@@ -248,8 +329,7 @@ impl Suite {
248329
let (shared_secret, enc) = self.encap(py, public_key)?;
249330
let (key, base_nonce) = self.key_schedule(py, shared_secret.as_bytes(), info_bytes)?;
250331

251-
let aesgcm = AesGcm::new(py, pyo3::types::PyBytes::new(py, &key).unbind().into_any())?;
252-
let ct = aesgcm.encrypt(py, CffiBuf::from_bytes(py, &base_nonce), plaintext, aad)?;
332+
let ct = self.aead_encrypt(py, &key, &base_nonce, plaintext, aad)?;
253333

254334
let enc_bytes = enc.as_bytes();
255335
let ct_bytes = ct.as_bytes();
@@ -273,7 +353,7 @@ impl Suite {
273353
aad: Option<CffiBuf<'_>>,
274354
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
275355
let ct_bytes = ciphertext.as_bytes();
276-
if ct_bytes.len() < kem_params::X25519_NENC + aead_params::AES_128_GCM_NT {
356+
if ct_bytes.len() < kem_params::X25519_NENC + self.aead.tag_length() {
277357
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
278358
}
279359

@@ -286,23 +366,17 @@ impl Suite {
286366
.map_err(|_| CryptographyError::from(exceptions::InvalidTag::new_err(())))?;
287367
let (key, base_nonce) = self.key_schedule(py, shared_secret.as_bytes(), info_bytes)?;
288368

289-
let aesgcm = AesGcm::new(py, pyo3::types::PyBytes::new(py, &key).unbind().into_any())?;
290-
aesgcm.decrypt(
291-
py,
292-
CffiBuf::from_bytes(py, &base_nonce),
293-
CffiBuf::from_bytes(py, ct),
294-
aad,
295-
)
369+
self.aead_decrypt(py, &key, &base_nonce, ct, aad)
296370
}
297371

298-
fn key_schedule(
372+
fn key_schedule<'p>(
299373
&self,
300-
py: pyo3::Python<'_>,
374+
py: pyo3::Python<'p>,
301375
shared_secret: &[u8],
302376
info: &[u8],
303377
) -> CryptographyResult<(
304-
[u8; aead_params::AES_128_GCM_NK],
305-
[u8; aead_params::AES_128_GCM_NN],
378+
pyo3::Bound<'p, pyo3::types::PyBytes>,
379+
pyo3::Bound<'p, pyo3::types::PyBytes>,
306380
)> {
307381
let psk_id_hash = self.hpke_labeled_extract(py, None, b"psk_id_hash", b"")?;
308382
let info_hash = self.hpke_labeled_extract(py, None, b"info_hash", info)?;
@@ -312,34 +386,29 @@ impl Suite {
312386

313387
let secret = self.hpke_labeled_extract(py, Some(shared_secret), b"secret", b"")?;
314388

315-
let key_vec = self.hpke_labeled_expand(
389+
let key = self.hpke_labeled_expand(
316390
py,
317391
&secret,
318392
b"key",
319393
&key_schedule_context,
320-
aead_params::AES_128_GCM_NK,
394+
self.aead.key_length(),
321395
)?;
322-
let nonce_vec = self.hpke_labeled_expand(
396+
let base_nonce = self.hpke_labeled_expand(
323397
py,
324398
&secret,
325399
b"base_nonce",
326400
&key_schedule_context,
327-
aead_params::AES_128_GCM_NN,
401+
self.aead.nonce_length(),
328402
)?;
329403

330-
let mut key = [0u8; aead_params::AES_128_GCM_NK];
331-
let mut base_nonce = [0u8; aead_params::AES_128_GCM_NN];
332-
key.copy_from_slice(key_vec.as_bytes());
333-
base_nonce.copy_from_slice(nonce_vec.as_bytes());
334-
335404
Ok((key, base_nonce))
336405
}
337406
}
338407

339408
#[pyo3::pymethods]
340409
impl Suite {
341410
#[new]
342-
fn new(_kem: KEM, _kdf: KDF, _aead: AEAD) -> CryptographyResult<Suite> {
411+
fn new(_kem: KEM, _kdf: KDF, aead: AEAD) -> CryptographyResult<Suite> {
343412
// Build suite IDs
344413
let mut kem_suite_id = [0u8; 5];
345414
kem_suite_id[..3].copy_from_slice(b"KEM");
@@ -349,9 +418,10 @@ impl Suite {
349418
hpke_suite_id[..4].copy_from_slice(b"HPKE");
350419
hpke_suite_id[4..6].copy_from_slice(&kem_params::X25519_ID.to_be_bytes());
351420
hpke_suite_id[6..8].copy_from_slice(&kdf_params::HKDF_SHA256_ID.to_be_bytes());
352-
hpke_suite_id[8..10].copy_from_slice(&aead_params::AES_128_GCM_ID.to_be_bytes());
421+
hpke_suite_id[8..10].copy_from_slice(&aead.id().to_be_bytes());
353422

354423
Ok(Suite {
424+
aead,
355425
kem_suite_id,
356426
hpke_suite_id,
357427
})

tests/hazmat/primitives/test_hpke.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
SUPPORTED_SUITES = [
2525
(KEM.X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM),
26+
(KEM.X25519, KDF.HKDF_SHA256, AEAD.CHACHA20_POLY1305),
2627
]
2728

2829

@@ -224,18 +225,25 @@ def test_vector_decryption(self, subtests):
224225
lambda f: json.load(f),
225226
)
226227

228+
aead_map = {
229+
0x0001: AEAD.AES_128_GCM,
230+
0x0003: AEAD.CHACHA20_POLY1305,
231+
}
232+
227233
for vector in vectors:
228-
# Currently support: mode 0 (Base), X25519, HKDF-SHA256, AES-GCM
234+
# Support: mode 0 (Base), X25519, HKDF-SHA256,
235+
# AES-128-GCM or ChaCha20Poly1305
229236
if not (
230237
vector["mode"] == 0
231238
and vector["kem_id"] == 0x0020
232239
and vector["kdf_id"] == 0x0001
233-
and vector["aead_id"] == 0x0001
240+
and vector["aead_id"] in aead_map
234241
):
235242
continue
236243

237244
with subtests.test():
238-
suite = Suite(KEM.X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM)
245+
aead = aead_map[vector["aead_id"]]
246+
suite = Suite(KEM.X25519, KDF.HKDF_SHA256, aead)
239247

240248
sk_r_bytes = bytes.fromhex(vector["skRm"])
241249
sk_r = x25519.X25519PrivateKey.from_private_bytes(sk_r_bytes)

0 commit comments

Comments
 (0)