From 323bc4ae5e22b26d5aac37c6803a846b7503f1e1 Mon Sep 17 00:00:00 2001 From: shanyu Date: Sat, 3 Oct 2026 05:45:32 +0800 Subject: [PATCH 1/2] Accept AEAD associated data on the HPKE single-shot API. info is mixed into the key schedule. aad is authenticated with the ciphertext and was previously only available on the private helpers. --- CHANGELOG.rst | 4 ++++ docs/hazmat/primitives/hpke.rst | 9 +++++++-- .../hazmat/bindings/_rust/openssl/hpke.pyi | 2 ++ src/rust/src/backend/hpke.rs | 10 ++++++---- tests/hazmat/primitives/test_hpke.py | 12 +++++++----- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c2dfd026b986..283df994ff0f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,10 @@ Changelog .. note:: This version is not yet released and is under active development. +* :meth:`~cryptography.hazmat.primitives.hpke.Suite.encrypt` and + :meth:`~cryptography.hazmat.primitives.hpke.Suite.decrypt` accept ``aad``, + additional authenticated data for the AEAD. It is separate from ``info``, + which is mixed into the key schedule. * :func:`~cryptography.hazmat.primitives.serialization.load_der_private_key` and :func:`~cryptography.hazmat.primitives.serialization.load_pem_private_key` now accept :rfc:`5958` ``OneAsymmetricKey`` (PKCS8 version 2). diff --git a/docs/hazmat/primitives/hpke.rst b/docs/hazmat/primitives/hpke.rst index 3c22f24e9928..9ae5826b3564 100644 --- a/docs/hazmat/primitives/hpke.rst +++ b/docs/hazmat/primitives/hpke.rst @@ -50,7 +50,7 @@ specifying auxiliary authenticated information. :param aead: The authenticated encryption algorithm. :type aead: :class:`AEAD` - .. method:: encrypt(plaintext, public_key, info=b"") + .. method:: encrypt(plaintext, public_key, info=b"", aad=b"") Encrypt a message using HPKE. @@ -70,10 +70,13 @@ specifying auxiliary authenticated information. and :class:`MLKEM1024P384PublicKey` for :attr:`KEM.MLKEM1024_P384`. :param bytes info: Application-specific context string for binding the encryption to a specific application or protocol. + :param bytes aad: Additional authenticated data passed to the AEAD. + It is authenticated with the ciphertext and is not mixed into the + key schedule. This is a different input from ``info``. :returns: The encapsulated key concatenated with ciphertext (enc || ct). :rtype: bytes - .. method:: decrypt(ciphertext, private_key, info=b"") + .. method:: decrypt(ciphertext, private_key, info=b"", aad=b"") Decrypt a message using HPKE. @@ -93,6 +96,8 @@ specifying auxiliary authenticated information. and :class:`MLKEM1024P384PrivateKey` for :attr:`KEM.MLKEM1024_P384`. :param bytes info: Application-specific context string (must match the value used during encryption). + :param bytes aad: Additional authenticated data (must match the value + used during encryption). :returns: The decrypted plaintext. :rtype: bytes :raises cryptography.exceptions.InvalidTag: If decryption fails. diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi index 7ea17112f373..7cd909cbb214 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi @@ -70,6 +70,7 @@ class Suite: | MLKEM768X25519PublicKey | MLKEM1024P384PublicKey, info: Buffer | None = None, + aad: Buffer | None = None, ) -> bytes: ... def decrypt( self, @@ -81,6 +82,7 @@ class Suite: | MLKEM768X25519PrivateKey | MLKEM1024P384PrivateKey, info: Buffer | None = None, + aad: Buffer | None = None, ) -> bytes: ... def _encrypt_with_aad( diff --git a/src/rust/src/backend/hpke.rs b/src/rust/src/backend/hpke.rs index d8055d31e0bd..163354db5775 100644 --- a/src/rust/src/backend/hpke.rs +++ b/src/rust/src/backend/hpke.rs @@ -1031,26 +1031,28 @@ impl Suite { }) } - #[pyo3(signature = (plaintext, public_key, info=None))] + #[pyo3(signature = (plaintext, public_key, info=None, aad=None))] fn encrypt<'p>( &self, py: pyo3::Python<'p>, plaintext: CffiBuf<'_>, public_key: &pyo3::Bound<'p, pyo3::PyAny>, info: Option>, + aad: Option>, ) -> CryptographyResult> { - self.encrypt_inner(py, plaintext, public_key, info, None) + self.encrypt_inner(py, plaintext, public_key, info, aad) } - #[pyo3(signature = (ciphertext, private_key, info=None))] + #[pyo3(signature = (ciphertext, private_key, info=None, aad=None))] fn decrypt<'p>( &self, py: pyo3::Python<'p>, ciphertext: CffiBuf<'_>, private_key: &pyo3::Bound<'p, pyo3::PyAny>, info: Option>, + aad: Option>, ) -> CryptographyResult> { - self.decrypt_inner(py, ciphertext, private_key, info, None) + self.decrypt_inner(py, ciphertext, private_key, info, aad) } } diff --git a/tests/hazmat/primitives/test_hpke.py b/tests/hazmat/primitives/test_hpke.py index 2f441eaf6c8b..881eff2f2263 100644 --- a/tests/hazmat/primitives/test_hpke.py +++ b/tests/hazmat/primitives/test_hpke.py @@ -400,14 +400,16 @@ def test_wrong_aad_fails(self): sk_r = x25519.X25519PrivateKey.generate() pk_r = sk_r.public_key() - ciphertext = rust_openssl.hpke._encrypt_with_aad( - suite, b"Secret message", pk_r, aad=b"correct aad" + ciphertext = suite.encrypt( + b"Secret message", pk_r, aad=b"correct aad" + ) + assert ( + suite.decrypt(ciphertext, sk_r, aad=b"correct aad") + == b"Secret message" ) with pytest.raises(InvalidTag): - rust_openssl.hpke._decrypt_with_aad( - suite, ciphertext, sk_r, aad=b"wrong aad" - ) + suite.decrypt(ciphertext, sk_r, aad=b"wrong aad") def test_info_mismatch_fails(self): suite = Suite(KEM.X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM) From edc76f31e3ec655a57f96d5fba91b7ce9dc7e1ad Mon Sep 17 00:00:00 2001 From: shanyu Date: Sat, 3 Oct 2026 09:54:05 +0800 Subject: [PATCH 2/2] Keep the HPKE associated-data call on one line. --- tests/hazmat/primitives/test_hpke.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/hazmat/primitives/test_hpke.py b/tests/hazmat/primitives/test_hpke.py index 881eff2f2263..19775d4b0b6d 100644 --- a/tests/hazmat/primitives/test_hpke.py +++ b/tests/hazmat/primitives/test_hpke.py @@ -400,9 +400,7 @@ def test_wrong_aad_fails(self): sk_r = x25519.X25519PrivateKey.generate() pk_r = sk_r.public_key() - ciphertext = suite.encrypt( - b"Secret message", pk_r, aad=b"correct aad" - ) + ciphertext = suite.encrypt(b"Secret message", pk_r, aad=b"correct aad") assert ( suite.decrypt(ciphertext, sk_r, aad=b"correct aad") == b"Secret message"