Skip to content

Commit 3301454

Browse files
committed
Remove aad parameter from HPKE one-shot public API
Per RFC 9180 Section 8.1, applications using single-shot APIs should use the info parameter for auxiliary authenticated information rather than aad. This change: - Removes aad parameter from public Suite.encrypt/decrypt in Rust - Adds module-level _encrypt_with_aad/_decrypt_with_aad functions in the private Rust hpke module for test vector validation - Updates tests to use the module-level functions for aad - Updates type stubs, documentation, and CHANGELOG Fixes #14073
1 parent 7e2ecc8 commit 3301454

File tree

5 files changed

+132
-63
lines changed

5 files changed

+132
-63
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ Changelog
8484
:class:`~cryptography.hazmat.primitives.asymmetric.utils.NoDigestInfo`.
8585
* Added :meth:`~cryptography.hazmat.primitives.hashes.Hash.hash`, a one-shot
8686
method for computing hashes.
87+
* Added :doc:`/hazmat/primitives/hpke` support implementing :rfc:`9180` for
88+
hybrid authenticated encryption.
8789

8890
.. _v46-0-5:
8991

docs/hazmat/primitives/hpke.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ ephemeral key pair, so encrypting the same plaintext twice will produce
1919
different ciphertext.
2020

2121
The ``info`` parameter should be used to bind the encryption to a specific
22-
context (e.g., "MyApp-v1-UserMessages"). The ``aad`` parameter provides
23-
additional authenticated data that is not encrypted but is authenticated
24-
along with the ciphertext.
22+
context (e.g., "MyApp-v1-UserMessages"). Per :rfc:`9180#section-8.1`,
23+
applications using single-shot APIs should use the ``info`` parameter for
24+
specifying auxiliary authenticated information.
2525

2626
.. code-block:: python
2727
@@ -51,27 +51,27 @@ along with the ciphertext.
5151
:param aead: The authenticated encryption algorithm.
5252
:type aead: :class:`AEAD`
5353

54-
.. method:: encrypt(plaintext, public_key, info=b"", aad=b"")
54+
.. method:: encrypt(plaintext, public_key, info=b"")
5555

5656
Encrypt a message using HPKE.
5757

5858
:param bytes plaintext: The message to encrypt.
5959
:param public_key: The recipient's public key.
6060
:type public_key: :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
61-
:param bytes info: Application-specific info string.
62-
:param bytes aad: Additional authenticated data.
61+
:param bytes info: Application-specific context string for binding the
62+
encryption to a specific application or protocol.
6363
:returns: The encapsulated key concatenated with ciphertext (enc || ct).
6464
:rtype: bytes
6565

66-
.. method:: decrypt(ciphertext, private_key, info=b"", aad=b"")
66+
.. method:: decrypt(ciphertext, private_key, info=b"")
6767

6868
Decrypt a message using HPKE.
6969

7070
:param bytes ciphertext: The enc || ct value from :meth:`encrypt`.
7171
:param private_key: The recipient's private key.
7272
:type private_key: :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey`
73-
:param bytes info: Application-specific info string.
74-
:param bytes aad: Additional authenticated data.
73+
:param bytes info: Application-specific context string (must match the
74+
value used during encryption).
7575
:returns: The decrypted plaintext.
7676
:rtype: bytes
7777
:raises cryptography.exceptions.InvalidTag: If decryption fails.

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,25 @@ class Suite:
2121
plaintext: Buffer,
2222
public_key: x25519.X25519PublicKey,
2323
info: Buffer | None = None,
24-
aad: Buffer | None = None,
2524
) -> bytes: ...
2625
def decrypt(
2726
self,
2827
ciphertext: Buffer,
2928
private_key: x25519.X25519PrivateKey,
3029
info: Buffer | None = None,
31-
aad: Buffer | None = None,
3230
) -> bytes: ...
31+
32+
def _encrypt_with_aad(
33+
suite: Suite,
34+
plaintext: Buffer,
35+
public_key: x25519.X25519PublicKey,
36+
info: Buffer | None = None,
37+
aad: Buffer | None = None,
38+
) -> bytes: ...
39+
def _decrypt_with_aad(
40+
suite: Suite,
41+
ciphertext: Buffer,
42+
private_key: x25519.X25519PrivateKey,
43+
info: Buffer | None = None,
44+
aad: Buffer | None = None,
45+
) -> bytes: ...

src/rust/src/backend/hpke.rs

Lines changed: 91 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod aead_params {
3535
frozen,
3636
eq,
3737
hash,
38+
from_py_object,
3839
module = "cryptography.hazmat.bindings._rust.openssl.hpke"
3940
)]
4041
#[derive(Clone, PartialEq, Eq, Hash)]
@@ -48,6 +49,7 @@ pub(crate) enum KEM {
4849
frozen,
4950
eq,
5051
hash,
52+
from_py_object,
5153
module = "cryptography.hazmat.bindings._rust.openssl.hpke"
5254
)]
5355
#[derive(Clone, PartialEq, Eq, Hash)]
@@ -61,6 +63,7 @@ pub(crate) enum KDF {
6163
frozen,
6264
eq,
6365
hash,
66+
from_py_object,
6467
module = "cryptography.hazmat.bindings._rust.openssl.hpke"
6568
)]
6669
#[derive(Clone, PartialEq, Eq, Hash)]
@@ -231,6 +234,64 @@ impl Suite {
231234
self.hkdf_expand(py, prk, &labeled_info, length)
232235
}
233236

237+
fn encrypt_inner<'p>(
238+
&self,
239+
py: pyo3::Python<'p>,
240+
plaintext: CffiBuf<'_>,
241+
public_key: &pyo3::Bound<'_, pyo3::PyAny>,
242+
info: Option<CffiBuf<'_>>,
243+
aad: Option<CffiBuf<'_>>,
244+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
245+
let info_bytes: &[u8] = info.as_ref().map(|b| b.as_bytes()).unwrap_or(b"");
246+
247+
let (shared_secret, enc) = self.encap(py, public_key)?;
248+
let (key, base_nonce) = self.key_schedule(py, shared_secret.as_bytes(), info_bytes)?;
249+
250+
let aesgcm = AesGcm::new(py, pyo3::types::PyBytes::new(py, &key).unbind().into_any())?;
251+
let ct = aesgcm.encrypt(py, CffiBuf::from_bytes(py, &base_nonce), plaintext, aad)?;
252+
253+
let enc_bytes = enc.as_bytes();
254+
let ct_bytes = ct.as_bytes();
255+
Ok(pyo3::types::PyBytes::new_with(
256+
py,
257+
enc_bytes.len() + ct_bytes.len(),
258+
|buf| {
259+
buf[..enc_bytes.len()].copy_from_slice(enc_bytes);
260+
buf[enc_bytes.len()..].copy_from_slice(ct_bytes);
261+
Ok(())
262+
},
263+
)?)
264+
}
265+
266+
fn decrypt_inner<'p>(
267+
&self,
268+
py: pyo3::Python<'p>,
269+
ciphertext: CffiBuf<'_>,
270+
private_key: &pyo3::Bound<'_, pyo3::PyAny>,
271+
info: Option<CffiBuf<'_>>,
272+
aad: Option<CffiBuf<'_>>,
273+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
274+
let ct_bytes = ciphertext.as_bytes();
275+
if ct_bytes.len() < kem_params::X25519_NENC {
276+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
277+
}
278+
279+
let info_bytes: &[u8] = info.as_ref().map(|b| b.as_bytes()).unwrap_or(b"");
280+
281+
let (enc, ct) = ct_bytes.split_at(kem_params::X25519_NENC);
282+
283+
let shared_secret = self.decap(py, enc, private_key)?;
284+
let (key, base_nonce) = self.key_schedule(py, shared_secret.as_bytes(), info_bytes)?;
285+
286+
let aesgcm = AesGcm::new(py, pyo3::types::PyBytes::new(py, &key).unbind().into_any())?;
287+
aesgcm.decrypt(
288+
py,
289+
CffiBuf::from_bytes(py, &base_nonce),
290+
CffiBuf::from_bytes(py, ct),
291+
aad,
292+
)
293+
}
294+
234295
fn key_schedule(
235296
&self,
236297
py: pyo3::Python<'_>,
@@ -293,69 +354,57 @@ impl Suite {
293354
})
294355
}
295356

296-
#[pyo3(signature = (plaintext, public_key, info=None, aad=None))]
357+
#[pyo3(signature = (plaintext, public_key, info=None))]
297358
fn encrypt<'p>(
298359
&self,
299360
py: pyo3::Python<'p>,
300361
plaintext: CffiBuf<'_>,
301362
public_key: &pyo3::Bound<'_, pyo3::PyAny>,
302363
info: Option<CffiBuf<'_>>,
303-
aad: Option<CffiBuf<'_>>,
304364
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
305-
let info_bytes: &[u8] = info.as_ref().map(|b| b.as_bytes()).unwrap_or(b"");
306-
307-
let (shared_secret, enc) = self.encap(py, public_key)?;
308-
let (key, base_nonce) = self.key_schedule(py, shared_secret.as_bytes(), info_bytes)?;
309-
310-
let aesgcm = AesGcm::new(py, pyo3::types::PyBytes::new(py, &key).unbind().into_any())?;
311-
let ct = aesgcm.encrypt(py, CffiBuf::from_bytes(py, &base_nonce), plaintext, aad)?;
312-
313-
let enc_bytes = enc.as_bytes();
314-
let ct_bytes = ct.as_bytes();
315-
Ok(pyo3::types::PyBytes::new_with(
316-
py,
317-
enc_bytes.len() + ct_bytes.len(),
318-
|buf| {
319-
buf[..enc_bytes.len()].copy_from_slice(enc_bytes);
320-
buf[enc_bytes.len()..].copy_from_slice(ct_bytes);
321-
Ok(())
322-
},
323-
)?)
365+
self.encrypt_inner(py, plaintext, public_key, info, None)
324366
}
325367

326-
#[pyo3(signature = (ciphertext, private_key, info=None, aad=None))]
368+
#[pyo3(signature = (ciphertext, private_key, info=None))]
327369
fn decrypt<'p>(
328370
&self,
329371
py: pyo3::Python<'p>,
330372
ciphertext: CffiBuf<'_>,
331373
private_key: &pyo3::Bound<'_, pyo3::PyAny>,
332374
info: Option<CffiBuf<'_>>,
333-
aad: Option<CffiBuf<'_>>,
334375
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
335-
let ct_bytes = ciphertext.as_bytes();
336-
if ct_bytes.len() < kem_params::X25519_NENC {
337-
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
338-
}
339-
340-
let info_bytes: &[u8] = info.as_ref().map(|b| b.as_bytes()).unwrap_or(b"");
341-
342-
let (enc, ct) = ct_bytes.split_at(kem_params::X25519_NENC);
376+
self.decrypt_inner(py, ciphertext, private_key, info, None)
377+
}
378+
}
343379

344-
let shared_secret = self.decap(py, enc, private_key)?;
345-
let (key, base_nonce) = self.key_schedule(py, shared_secret.as_bytes(), info_bytes)?;
380+
#[pyo3::pyfunction]
381+
#[pyo3(signature = (suite, plaintext, public_key, info=None, aad=None))]
382+
fn _encrypt_with_aad<'p>(
383+
py: pyo3::Python<'p>,
384+
suite: &Suite,
385+
plaintext: CffiBuf<'_>,
386+
public_key: &pyo3::Bound<'_, pyo3::PyAny>,
387+
info: Option<CffiBuf<'_>>,
388+
aad: Option<CffiBuf<'_>>,
389+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
390+
suite.encrypt_inner(py, plaintext, public_key, info, aad)
391+
}
346392

347-
let aesgcm = AesGcm::new(py, pyo3::types::PyBytes::new(py, &key).unbind().into_any())?;
348-
aesgcm.decrypt(
349-
py,
350-
CffiBuf::from_bytes(py, &base_nonce),
351-
CffiBuf::from_bytes(py, ct),
352-
aad,
353-
)
354-
}
393+
#[pyo3::pyfunction]
394+
#[pyo3(signature = (suite, ciphertext, private_key, info=None, aad=None))]
395+
fn _decrypt_with_aad<'p>(
396+
py: pyo3::Python<'p>,
397+
suite: &Suite,
398+
ciphertext: CffiBuf<'_>,
399+
private_key: &pyo3::Bound<'_, pyo3::PyAny>,
400+
info: Option<CffiBuf<'_>>,
401+
aad: Option<CffiBuf<'_>>,
402+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
403+
suite.decrypt_inner(py, ciphertext, private_key, info, aad)
355404
}
356405

357406
#[pyo3::pymodule(gil_used = false)]
358407
pub(crate) mod hpke {
359408
#[pymodule_export]
360-
use super::{Suite, AEAD, KDF, KEM};
409+
use super::{Suite, _decrypt_with_aad, _encrypt_with_aad, AEAD, KDF, KEM};
361410
}

tests/hazmat/primitives/test_hpke.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99

1010
from cryptography.exceptions import InvalidTag
11+
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
1112
from cryptography.hazmat.primitives.asymmetric import x25519
1213
from cryptography.hazmat.primitives.hpke import (
1314
AEAD,
@@ -49,17 +50,13 @@ def test_roundtrip(self, kem, kdf, aead):
4950
sk_r = x25519.X25519PrivateKey.generate()
5051
pk_r = sk_r.public_key()
5152

52-
ciphertext = suite.encrypt(
53-
b"Hello, HPKE!", pk_r, info=b"test", aad=b"additional data"
54-
)
55-
plaintext = suite.decrypt(
56-
ciphertext, sk_r, info=b"test", aad=b"additional data"
57-
)
53+
ciphertext = suite.encrypt(b"Hello, HPKE!", pk_r, info=b"test")
54+
plaintext = suite.decrypt(ciphertext, sk_r, info=b"test")
5855

5956
assert plaintext == b"Hello, HPKE!"
6057

6158
@pytest.mark.parametrize("kem,kdf,aead", SUPPORTED_SUITES)
62-
def test_roundtrip_no_aad(self, kem, kdf, aead):
59+
def test_roundtrip_no_info(self, kem, kdf, aead):
6360
suite = Suite(kem, kdf, aead)
6461

6562
sk_r = x25519.X25519PrivateKey.generate()
@@ -88,10 +85,14 @@ def test_wrong_aad_fails(self):
8885
sk_r = x25519.X25519PrivateKey.generate()
8986
pk_r = sk_r.public_key()
9087

91-
ciphertext = suite.encrypt(b"Secret message", pk_r, aad=b"correct aad")
88+
ciphertext = rust_openssl.hpke._encrypt_with_aad(
89+
suite, b"Secret message", pk_r, aad=b"correct aad"
90+
)
9291

9392
with pytest.raises(InvalidTag):
94-
suite.decrypt(ciphertext, sk_r, aad=b"wrong aad")
93+
rust_openssl.hpke._decrypt_with_aad(
94+
suite, ciphertext, sk_r, aad=b"wrong aad"
95+
)
9596

9697
def test_info_mismatch_fails(self):
9798
suite = Suite(KEM.X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM)
@@ -204,6 +205,10 @@ def test_vector_decryption(self, subtests):
204205
pt_expected = bytes.fromhex(encryption["pt"])
205206

206207
# Combine enc || ct for single-shot decrypt
208+
# Use internal function with AAD for test vector
209+
# validation
207210
ciphertext = enc + ct
208-
pt = suite.decrypt(ciphertext, sk_r, info=info, aad=aad)
211+
pt = rust_openssl.hpke._decrypt_with_aad(
212+
suite, ciphertext, sk_r, info=info, aad=aad
213+
)
209214
assert pt == pt_expected

0 commit comments

Comments
 (0)