Hybrid Public Key Encryption (HPKE)¶
HPKE (RFC 9180) is a non-interactive protocol that enables secure communication using a combination of public key cryptography (a Key Encapsulation Mechanism, KEM) and symmetric encryption (Authenticated Encryption with Associated Data, AEAD).
It allows a sender that knows the public key of the intended recipient to encrypt one or more messages, ensuring confidentiality and integrity, without any interaction with the recipient.
There are no significant limits on the size of a single message or on the number of messages in the sequence. Along with each encrypted message (ciphertext), the sender can also deliver a piece of non-sensitive data (e.g., a header), which is authenticated together with the matching message (additional authenticated data, AAD).
Before or together with the first message, the sender must deliver the encapsulated session secret (enc) to the receiver.
In the basic scheme above, the receiver gets no proof of the sender’s identity. Variants of the protocol exist to enable the receiver to ascertain the origin of the message.
First variant: Auth mode. The sender can contribute its own private key to the encryption. The receiver must have the matching public key:
Second variant: PSK mode. The sender and the receiver can agree on a secret key (a pre-shared key, PSK, of at least 32 random bytes) and use it for both encryption and decryption.
Finally, the two authentication modes can also be combined (AuthPSK mode).
Note
The PSK is only used to authenticate the messages; it is not a replacement for the asymmetric key that the receiver must provide to the sender.
Note
Future versions of HPKE will drop support for Auth and AuthPSK modes.
Examples¶
This is how the sender can encrypt two messages:
from Crypto.Protocol import HPKE
# Let's assume that the recipient has delivered its public key,
# which is already loaded in a variable called their_pub_key
#
# For the sake of clarity, it could be simulated with:
#
# from Crypto.PublicKey import ECC
# their_key = ECC.generate(curve='p256')
# their_pub_key = their_key.public_key()
encryptor = HPKE.new(receiver_key=their_pub_key,
aead_id=HPKE.AEAD.AES128_GCM)
ct_1 = encryptor.seal(b'Message 1')
ct_2 = encryptor.seal(b'Message 2')
# The sender will deliver:
# - encryptor.enc as enc
# - ct_1
# - ct_2
And this is how the receiver can decrypt them:
from Crypto.Protocol import HPKE
# Let's assume that the recipient has its own private key,
# which is already loaded in a variable called my_key
#
# To continue the simulation, assign:
#
# my_key = their_key
decryptor = HPKE.new(receiver_key=my_key,
aead_id=HPKE.AEAD.AES128_GCM,
enc=enc)
# Any of the calls to unseal() can raise ValueError
# if it detects a key mismatch, a modified message, or messages out of order
pt1 = decryptor.unseal(ct_1)
pt2 = decryptor.unseal(ct_2)
HPKE can also derive shared secret material without encrypting messages:
from Crypto.Protocol import HPKE
exporter = HPKE.new(receiver_key=their_pub_key,
aead_id=HPKE.AEAD.EXPORT_ONLY)
secret = exporter.export(b'example context', 32)
# The sender will deliver exporter.enc as enc.
# The receiver will create the same context with its private key
# and call export() with the same context and length.
Post-quantum HPKE¶
HPKE based on ECC keys (DHKEM) is not secure against quantum computers. An extension of HPKE, still at the draft stage (draft-ietf-hpke-pq), defines how to use post-quantum cryptographic keys:
an ML-KEM key, or
a hybrid key of the hybrid KEM X-Wing, which combines ML-KEM and X25519 and remains secure as long as at least one of them is secure.
The KEM (and with it the KDF) is selected by the type of the receiver key:
Receiver key |
KEM |
KDF |
Size of enc |
|---|---|---|---|
ECC, |
DHKEM(P-256) |
HKDF-SHA256 |
65 bytes |
ECC, |
DHKEM(P-384) |
HKDF-SHA384 |
97 bytes |
ECC, |
DHKEM(P-521) |
HKDF-SHA512 |
133 bytes |
ECC, |
DHKEM(X25519) |
HKDF-SHA256 |
32 bytes |
ECC, |
DHKEM(X448) |
HKDF-SHA512 |
56 bytes |
ML-KEM, |
ML-KEM-512 |
HKDF-SHA256 |
768 bytes |
ML-KEM, |
ML-KEM-768 |
HKDF-SHA256 |
1088 bytes |
ML-KEM, |
ML-KEM-1024 |
HKDF-SHA384 |
1568 bytes |
Hybrid, |
MLKEM768-X25519 |
HKDF-SHA256 |
1120 bytes |
The hybrid KEMs MLKEM768-P256 and MLKEM1024-P384 of draft-ietf-hpke-pq are not supported yet.
With ML-KEM and X-Wing, only the Base and PSK modes are available:
the Auth and AuthPSK modes are not supported (i.e., sender_key must not be specified).
The rest of the API is the same:
from Crypto.Protocol import HPKE
from Crypto.PublicKey import Hybrid
# The receiver
my_key = Hybrid.generate(param_set=Hybrid.MLKEM768_X25519)
their_pub_key = my_key.public_key()
# The sender
encryptor = HPKE.new(receiver_key=their_pub_key,
aead_id=HPKE.AEAD.CHACHA20_POLY1305)
ct = encryptor.seal(b'Message 1')
# The receiver again
decryptor = HPKE.new(receiver_key=my_key,
aead_id=HPKE.AEAD.CHACHA20_POLY1305,
enc=encryptor.enc)
pt = decryptor.unseal(ct)
Specification¶
- class Crypto.Protocol.HPKE.AEAD(value)¶
The symmetric ciphers of HPKE (Authenticated Encryption with Associated Data, AEAD), for the parameter
aead_idofnew(). The value of each member is its HPKE identifier.- AES128_GCM = 1¶
AES-128 in GCM mode
- AES256_GCM = 2¶
AES-256 in GCM mode
- CHACHA20_POLY1305 = 3¶
ChaCha20-Poly1305
- EXPORT_ONLY = 65535¶
No cipher, so the HPKE context can only export secrets (see
HPKE_Cipher.export())
- Crypto.Protocol.HPKE.new(*, receiver_key, aead_id, enc=None, sender_key=None, psk=None, info=None)¶
Create an HPKE context which can be used:
by the sender to seal (encrypt) a message,
by the receiver to unseal (decrypt) it, or
by either party to export shared secret material.
As a minimum, the two parties agree on the receiver’s asymmetric key (of which the sender will only know the public half).
Additionally, for authentication purposes, they may also agree on:
the sender’s asymmetric key (of which the receiver will only know the public half)
a shared secret (e.g., a symmetric key derived from a password)
- Parameters:
receiver_key (
Union[EccKey,MLKEMKey,HybridKey]) –The key of the receiver, which determines the KEM. One of:
an ECC key (
Crypto.PublicKey.ECC.EccKey), for DHKEM, on one of the following curves:NIST P-256,NIST P-384,NIST P-521,X25519orX448;an ML-KEM key (
Crypto.PublicKey.MLKEM.MLKEMKey), of any parameter set (post-quantum);a hybrid key (
Crypto.PublicKey.Hybrid.HybridKey) of the hybrid KEM X-Wing, which combines ML-KEM and X25519 (post-quantum/traditional hybrid).
If this is a public key, the HPKE context can only be used to seal (encrypt) or export a secret.
If this is a private key, the HPKE context can only be used to unseal (decrypt) or export a secret.
aead_id (
AEAD) –The HPKE identifier of the symmetric cipher. The possible values are:
HPKE.AEAD.AES128_GCMHPKE.AEAD.AES256_GCMHPKE.AEAD.CHACHA20_POLY1305HPKE.AEAD.EXPORT_ONLY
The encapsulated key (the KEM ciphertext) that the sender delivered, available to the sender as
HPKE_Cipher.enc.The receiver must always specify this parameter.
The sender must always omit this parameter.
sender_key (
Optional[EccKey]) – The ECC key of the sender. It must be on the same curve as thereceiver_key. If thereceiver_keyis a public key,sender_keymust be a private key, and vice versa. It cannot be used with ML-KEM or hybrid keys (they have no authenticated mode).psk (
Optional[tuple[bytes,bytes]]) –A Pre-Shared Key (PSK) as a 2-tuple of non-empty byte strings: the identifier and the actual secret value. Sender and receiver must use the same PSK (or none).
The secret value must be at least 32 bytes long, but it must not be a low-entropy password (use a KDF like PBKDF2 or scrypt to derive a secret from a password).
info (
Optional[bytes]) – A non-secret parameter that contributes to the generation of all session keys. Sender and receiver must use the same info parameter (or none).
- Return type:
- Returns:
An object that can be used for sealing (if
receiver_keyis a public key) or unsealing (ifreceiver_keyis a private key), and in both cases for exporting secrets (withHPKE.AEAD.EXPORT_ONLY, only for that). For the receiver, correctness of all the keys and parameters will only be assessed with the first call tounseal(), and never byexport().
- class Crypto.Protocol.HPKE.HPKE_Cipher(receiver_key, enc, sender_key, psk_pair, info, aead_id, mode)¶
An HPKE context, to seal or unseal messages, or to export secrets.
Do not instantiate directly: use
new()instead.- export(exporter_context, length)¶
Export a secret from this HPKE context.
This method can be invoked multiple times, with different contexts, to derive independent secrets.
- Parameters:
- Return type:
- Returns:
The exported secret.
Warning
Unlike
unseal(), this method never detects if sender and receiver used different keys or parameters: it simply returns a different secret. If needed, the application must confirm that both parties derived the same secret.
- seal(plaintext, auth_data=None)¶
Encrypt and authenticate a message.
This method can be invoked multiple times to seal an ordered sequence of messages.
- unseal(ciphertext, auth_data=None)¶
Decrypt a message and validate its authenticity.
This method can be invoked multiple times to unseal an ordered sequence of messages.
- Parameters:
- Return type:
- Returns:
The original plaintext.
- Raises:
ValueError – if the ciphertext (in combination with the AAD) is not valid. At the first call of
unseal(), it can also mean that one of the keys or parameters used to set up the context is wrong or missing.