Skip to main content

Crate hpke

Crate hpke 

Source
Expand description

HPKE: Hybrid Public Key Encryption, as specified in draft-ietf-hpke-hpke-05 (which obsoletes RFC 9180).

HPKE provides public-key encryption of arbitrary-sized plaintexts for a recipient public key. It also includes an authenticated variant that authenticates possession of a pre-shared key (PSK). The two asymmetric authenticated variants of RFC 9180 (mode_auth and mode_auth_psk) were removed from the specification and are not implemented.

A ciphersuite is a triple (KEM, KDF, AEAD) selected at compile time via type parameters. The crate ships the registered algorithms, the MLKEM768-X25519 hybrid post-quantum KEM (KEM id 0x647a) and the SHAKE256 single-stage KDF (KDF id 0x0011) from draft-ietf-hpke-pq-05:

Any of the three roles can be replaced by a custom implementation: define a type and implement kem::Kem, kdf::Kdf or aead::Aead for it.

§Example

One-shot encryption with the hybrid post-quantum MLKEM768-X25519 + SHAKE256 + ChaCha20-Poly1305 suite:

use hpke::{
    self, Mode,
    aead::ChaCha20Poly1305,
    kdf::Shake256,
    kem::Kem,
    kem::MLKEM768X25519,
};

let (bob_sk, bob_pk) = MLKEM768X25519::generate_keypair().unwrap();

let info = b"Alice and Bob's weekly chat";
let (encapped_key, mut sender) = hpke::new_sender::<MLKEM768X25519, Shake256, ChaCha20Poly1305>(
    &Mode::Base, &bob_pk, info,
).unwrap();
let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();

let mut recipient = hpke::new_recipient::<MLKEM768X25519, Shake256, ChaCha20Poly1305>(
    &Mode::Base, &bob_sk, &encapped_key, info,
).unwrap();
let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
assert_eq!(plaintext, b"fronthand or backhand?");

§Pre-shared key mode

In pre-shared key mode both parties are provisioned with a pre_shared_key (at least 32 bytes of entropy) and a pre_shared_key_id that identifies it. The pre-shared key strengthens confidentiality in certain adversary models and proves possession of the key, but it does not by itself say which sender produced a message. Use the sender-authentication construction below when the recipient must attribute a message to a specific sender.

use hpke::{
    self, Mode,
    aead::Aes256Gcm,
    kdf::HkdfSha256,
    kem::Kem,
    kem::X25519HkdfSha256,
};

let (bob_sk, bob_pk) = X25519HkdfSha256::generate_keypair().unwrap();
let pre_shared_key = [0x42u8; 32];
let pre_shared_key_id = b"weekly-chat";

let info = b"Alice and Bob's weekly chat";
let (encapped_key, mut sender) = hpke::new_sender::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
    &Mode::PreSharedKey { pre_shared_key: &pre_shared_key, pre_shared_key_id },
    &bob_pk, info,
).unwrap();
let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();

let mut recipient = hpke::new_recipient::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
    &Mode::PreSharedKey { pre_shared_key: &pre_shared_key, pre_shared_key_id },
    &bob_sk, &encapped_key, info,
).unwrap();
let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
assert_eq!(plaintext, b"fronthand or backhand?");

§Sender authentication

draft-ietf-hpke-hpke-05 removed the asymmetric authenticated modes of RFC 9180 (mode_auth and mode_auth_psk), so an HPKE ciphertext does not carry a sender signature. To let the recipient attribute a message to a specific sender, combine Base-mode HPKE with a signature scheme over the transmitted (encapped_key, ciphertext) tuple. This example uses ML-DSA-65 (FIPS 204), which is post-quantum; the same construction works with any signature scheme.

use crypto::mldsa::MlDsa65SecretKey;
use hpke::{
    self, Mode,
    aead::Aes256Gcm,
    kdf::HkdfSha256,
    kem::{Kem, X25519HkdfSha256},
};

// Alice's long-term ML-DSA-65 signing key pair; Bob holds the HPKE key pair.
let alice_signing = MlDsa65SecretKey::generate().unwrap();
let alice_signing_public = alice_signing.public_key();
let (bob_sk, bob_pk) = X25519HkdfSha256::generate_keypair().unwrap();

let info = b"Alice and Bob's weekly chat";
let (encapped_key, mut sender) = hpke::new_sender::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
    &Mode::Base, &bob_pk, info,
).unwrap();
let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();

// Alice signs the message `encapped_key || ciphertext` under a
// domain-separation context.
let mut encapped_key_bytes = [0u8; X25519HkdfSha256::ENCAPPED_KEY_SIZE];
X25519HkdfSha256::encapped_key_to_bytes(&mut encapped_key_bytes, &encapped_key).unwrap();
let mut transcript = encapped_key_bytes.to_vec();
transcript.extend_from_slice(&ciphertext);
let signature = alice_signing.sign(&transcript, b"HPKE sender auth").unwrap();

// Bob verifies the signature before decrypting and rejects on failure.
assert!(
    alice_signing_public
        .verify(&transcript, &signature, b"HPKE sender auth")
        .is_ok()
);

let mut recipient = hpke::new_recipient::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
    &Mode::Base, &bob_sk, &encapped_key, info,
).unwrap();
let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
assert_eq!(plaintext, b"fronthand or backhand?");

The signature covers the encapsulated key as well as the ciphertext, so a replayed or re-encapsulated message does not verify against the same signature. The recipient must verify (and reject on failure) before treating the plaintext as authenticated.

§Security

  • Messages must be sealed and opened in the same order. There is no tolerance for reordering or loss; per-message context (e.g. a counter) can be authenticated by passing it as associated_data.
  • Replaying an encapped_key reproduces the same context, so exported secrets are not replay protected. Do not derive an AEAD (key, nonce) pair from an exported secret; mix in fresh recipient-provided randomness (draft-ietf-hpke-hpke-05 Section 9.8).
  • Plaintexts larger than the AEAD’s MAX_PLAINTEXT_SIZE (or ciphertexts larger than MAX_CIPHERTEXT_SIZE) are rejected rather than processed, as draft-ietf-hpke-hpke-05 Section 5.2 requires. Sealing and opening also take &mut self, so nonce derivation, encryption, and sequence-number advancement cannot be interleaved on a shared context.
  • Pre-shared keys must contain at least 32 bytes of entropy. Low-entropy pre-shared keys are vulnerable to dictionary attacks.
  • There is no forward secrecy against recipient static key compromise: anyone with the recipient’s secret key can decrypt all past captures.

Re-exports§

pub use aead::Aead;
pub use kdf::Kdf;
pub use kem::Kem;

Modules§

aead
AEAD algorithms for HPKE ciphersuites (draft-ietf-hpke-hpke-05 Section 5.2 and Section 7.3).
kdf
Key derivation functions for HPKE (draft-ietf-hpke-hpke-05 Section 4 and Section 7.2).
kem
Key encapsulation mechanisms (KEMs) for HPKE (draft-ietf-hpke-hpke-05 Section 4 and Section 7.1).

Structs§

RecipientContext
The recipient’s decryption context, returned by new_recipient.
SenderContext
The sender’s encryption context, returned by new_sender.

Enums§

HpkeError
Errors that can occur during HPKE setup, key encapsulation, key derivation or message encryption/decryption.
Mode
The HPKE session mode, shared by the sender and recipient roles.

Functions§

new_recipient
Sets up a recipient context (draft-ietf-hpke-hpke-05 Section 5.1).
new_sender
Sets up a sender context (draft-ietf-hpke-hpke-05 Section 5.1).