A Python implementation of Hybrid Public Key Encryption (HPKE) as specified in RFC 9180
Project description
rfc9180-py
A Python implementation of Hybrid Public Key Encryption (HPKE) as specified in RFC 9180.
Overview
HPKE provides a variant of public key encryption for arbitrary-sized plaintexts. It combines an asymmetric Key Encapsulation Mechanism (KEM), a Key Derivation Function (KDF), and an Authenticated Encryption with Associated Data (AEAD) algorithm to provide secure encryption with optional authentication.
This library implements the core HPKE specification using Python's cryptography library, providing both single-shot and multi-shot APIs for all HPKE modes.
Features
- All HPKE Modes: Base, PSK, Auth, and AuthPSK modes
- Multiple Ciphersuites: Support for various KEM, KDF, and AEAD combinations
- Single-Shot APIs: Convenient one-call encryption/decryption
- Context APIs: Multi-message encryption contexts with automatic nonce management
- Secret Export: Export application secrets from rfc9180 contexts
- RFC 9180 Compliant: Follows the specification closely with proper domain separation and validation
- Key Management: Key generation, derivation, and serialization utilities
- Message Encoding Helpers: Self-describing message format with header encoding/parsing
Installation
pip install rfc9180-py
The library requires Python 3.9+ and uses the cryptography library for all cryptographic primitives.
Quick Start
from rfc9180 import HPKE, KEMID, KDFID, AEADID, create_hpke
# Create HPKE instance with default ciphersuite (X25519 + HKDF-SHA256 + AES-128-GCM)
hpke = HPKE(KEMID.DHKEM_X25519_HKDF_SHA256, KDFID.HKDF_SHA256, AEADID.AES_128_GCM)
# Or use the convenience function
hpke = create_hpke() # Uses defaults
# Generate recipient key pair
skR, pkR = hpke.generate_key_pair()
# Single-shot encryption (Base mode)
info = b"application-info"
aad = b"additional-data"
plaintext = b"secret message"
enc, ciphertext = hpke.seal_base(pkR, info, aad, plaintext)
# Single-shot decryption
decrypted = hpke.open_base(enc, skR, info, aad, ciphertext)
assert decrypted == plaintext
# Multi-shot encryption
enc, ctx_sender = hpke.setup.setup_base_sender(pkR, info)
ct1 = ctx_sender.seal(aad, b"message 1")
ct2 = ctx_sender.seal(aad, b"message 2")
# Multi-shot decryption
ctx_recipient = hpke.setup.setup_base_recipient(enc, skR, info)
pt1 = ctx_recipient.open(aad, ct1)
pt2 = ctx_recipient.open(aad, ct2)
# Export secrets
secret = ctx_sender.export(b"exporter-context", 32)
Implementation Status
✅ Implemented (RFC 9180 Compliance)
Section 4: Cryptographic Dependencies
- ✅ DHKEM (DH-Based KEM): Full implementation with ExtractAndExpand
- ✅ LabeledExtract: RFC 9180 §4 with proper "HPKE-v1" domain separation
- ✅ LabeledExpand: RFC 9180 §4 with length prefixing
Section 5: Hybrid Public Key Encryption
- ✅ SetupBaseS/R: Base mode setup (Section 5.1.1)
- ✅ SetupPSKS/R: PSK mode setup (Section 5.1.2)
- ✅ SetupAuthS/R: Auth mode setup (Section 5.1.3)
- ✅ SetupAuthPSKS/R: AuthPSK mode setup (Section 5.1.4)
- ✅ Key Schedule: Full implementation with proper PSK validation
- ✅ Context Seal/Open: Multi-message encryption with sequence number management
- ✅ Secret Export: Context export functionality (Section 5.3)
Section 6: Single-Shot APIs
- ✅ Seal/Open APIs: All four modes (Base, PSK, Auth, AuthPSK)
- ✅ Export APIs: Single-shot secret export for all modes
Section 7: Algorithm Identifiers
KEMs (Section 7.1):
- ✅ DHKEM_X25519_HKDF_SHA256 (0x0020): Full implementation
- ✅ Key generation
- ✅ Key derivation with RFC 7748 clamping
- ✅ Serialization/deserialization
- ✅ All-zero DH output validation
- ✅ DHKEM_P256_HKDF_SHA256 (0x0010): Full implementation
- ✅ Key generation
- ✅ Key derivation with rejection sampling
- ✅ Serialization/deserialization
- ✅ Partial public key validation
- ✅ DHKEM_P384_HKDF_SHA384 (0x0011): Full implementation
- ✅ Key generation
- ✅ Key derivation with rejection sampling
- ✅ Serialization/deserialization
- ✅ DHKEM_P521_HKDF_SHA512 (0x0012): Full implementation
- ✅ Key generation
- ✅ Key derivation with rejection sampling
- ✅ Serialization/deserialization
- ✅ DHKEM_X448_HKDF_SHA512 (0x0021): Full implementation
- ✅ Key generation
- ✅ Key derivation with RFC 7748 clamping
- ✅ Serialization/deserialization
KDFs (Section 7.2):
- ✅ HKDF_SHA256 (0x0001): Full implementation
- ✅ HKDF_SHA384 (0x0002): Full implementation
- ✅ HKDF_SHA512 (0x0003): Full implementation
AEADs (Section 7.3):
- ✅ AES_128_GCM (0x0001): Full implementation
- ✅ AES_256_GCM (0x0002): Full implementation
- ✅ CHACHA20_POLY1305 (0x0003): Full implementation
- ✅ EXPORT_ONLY (0xFFFF): Full implementation (export-only mode)
Section 7.1: KEM Operations
- ✅ SerializePublicKey/DeserializePublicKey: Implemented for all supported KEMs
- ✅ SerializePrivateKey/DeserializePrivateKey: Implemented for all supported KEMs
- ✅ DeriveKeyPair: Implemented with proper rejection sampling/clamping
- ✅ Validation: Input/output validation per Section 7.1.4
Section 8: API Considerations
- ✅ AAD Support: Full support for additional authenticated data
- ✅ Error Handling: Comprehensive exception hierarchy
Section 9: Security Considerations
- ✅ Domain Separation: Proper suite ID construction and labeled operations
- ✅ PSK Recommendations: Enforced minimum entropy requirements
- ✅ Nonce Management: Automatic sequence number handling with overflow protection
- ✅ Key Reuse: Proper ephemeral key generation per message
❌ Not Implemented
Section 10: Message Encoding
- ✅ Helpers Provided: This library provides application-level helpers to encode/decode messages with a self-describing header per RFC §10 guidance. See below.
Section 9.7: Application-Level Features (Non-Goals)
The following are explicitly not provided by HPKE and must be handled by applications:
- ❌ Message Ordering: No built-in message ordering (Section 9.7.1)
- ❌ Downgrade Prevention: No protocol version negotiation (Section 9.7.2)
- ❌ Replay Protection: No built-in replay detection (Section 9.7.3)
- ❌ Forward Secrecy: Requires application-level key rotation (Section 9.7.4)
- ❌ Plaintext Length Hiding: No padding mechanisms (Section 9.7.6)
Note: These are intentional non-goals per RFC 9180 and should be handled at the application layer.
Supported Ciphersuites
| KEM | KDF | AEAD | Status |
|---|---|---|---|
| X25519 | HKDF-SHA256 | AES-128-GCM | ✅ |
| X25519 | HKDF-SHA256 | AES-256-GCM | ✅ |
| X25519 | HKDF-SHA256 | ChaCha20-Poly1305 | ✅ |
| X25519 | HKDF-SHA256 | EXPORT_ONLY | ✅ |
| P-256 | HKDF-SHA256 | AES-128-GCM | ✅ |
| P-256 | HKDF-SHA256 | AES-256-GCM | ✅ |
| P-256 | HKDF-SHA256 | ChaCha20-Poly1305 | ✅ |
| P-256 | HKDF-SHA256 | EXPORT_ONLY | ✅ |
| P-384 | HKDF-SHA384 | AES-128-GCM | ✅ |
| P-384 | HKDF-SHA384 | AES-256-GCM | ✅ |
| P-384 | HKDF-SHA384 | ChaCha20-Poly1305 | ✅ |
| P-384 | HKDF-SHA384 | EXPORT_ONLY | ✅ |
| P-521 | HKDF-SHA512 | AES-128-GCM | ✅ |
| P-521 | HKDF-SHA512 | AES-256-GCM | ✅ |
| P-521 | HKDF-SHA512 | ChaCha20-Poly1305 | ✅ |
| P-521 | HKDF-SHA512 | EXPORT_ONLY | ✅ |
| X448 | HKDF-SHA512 | AES-128-GCM | ✅ |
| X448 | HKDF-SHA512 | AES-256-GCM | ✅ |
| X448 | HKDF-SHA512 | ChaCha20-Poly1305 | ✅ |
| X448 | HKDF-SHA512 | EXPORT_ONLY | ✅ |
API Documentation
Package Structure
The package is installed as rfc9180-py but imported as hpke:
from rfc9180 import HPKE, KEMID, KDFID, AEADID, create_hpke, append_header, parse_header
from rfc9180.constants import HPKEMode
from rfc9180.setup import HPKESetup
from rfc9180.single_shot import HPKESingleShot
from rfc9180.context import Context, ContextSender, ContextRecipient
from rfc9180.primitives.kem import DHKEM_X25519, DHKEM_X448, DHKEM_P256, DHKEM_P384, DHKEM_P521
from rfc9180.primitives.kdf import KDFBase
from rfc9180.primitives.aead import AEADBase
High-Level API
HPKE Class
The main HPKE class provides a convenient interface for all HPKE operations:
from rfc9180 import HPKE, KEMID, KDFID, AEADID
# Create HPKE instance
hpke = HPKE(kem_id, kdf_id, aead_id)
# Access primitives
hpke.kem_id # KEM identifier
hpke.kdf_id # KDF identifier
hpke.aead_id # AEAD identifier
hpke.kem # KEM instance
hpke.kdf # KDF instance
hpke.aead # AEAD instance
hpke.setup # Setup functions (HPKESetup instance)
Convenience Function
from rfc9180 import create_hpke
# Create with defaults (X25519 + HKDF-SHA256 + AES-128-GCM)
hpke = create_hpke()
# Or specify algorithms
hpke = create_hpke(
kem_id=KEMID.DHKEM_X25519_HKDF_SHA256,
kdf_id=KDFID.HKDF_SHA256,
aead_id=AEADID.AES_128_GCM
)
Key Management Methods
The HPKE class provides key management utilities:
# Generate a new key pair
skR, pkR = hpke.generate_key_pair()
# Derive a key pair from a seed
seed = b"your-seed-material" # Must be appropriate length for the KEM
skR, pkR = hpke.derive_key_pair(seed)
# Serialize keys
pk_bytes = hpke.serialize_public_key(pkR)
sk_bytes = hpke.serialize_private_key(skR)
# Keys can be passed as bytes or Key Objects - automatic deserialization
enc, ct = hpke.seal_base(pk_bytes, info, aad, pt) # Works with bytes
enc, ct = hpke.seal_base(pkR, info, aad, pt) # Works with Key Objects
Single-Shot APIs
Single-shot APIs combine setup and encryption/decryption in one call:
# Base mode
enc, ct = hpke.seal_base(pkR, info, aad, pt)
pt = hpke.open_base(enc, skR, info, aad, ct)
# PSK mode
psk = b"pre-shared-key" # Must have at least 32 bytes of entropy
psk_id = b"psk-identifier"
enc, ct = hpke.seal_psk(pkR, info, aad, pt, psk, psk_id)
pt = hpke.open_psk(enc, skR, info, aad, ct, psk, psk_id)
# Auth mode
skS, pkS = hpke.generate_key_pair() # Sender key pair
enc, ct = hpke.seal_auth(pkR, info, aad, pt, skS)
pt = hpke.open_auth(enc, skR, info, aad, ct, pkS)
# AuthPSK mode
enc, ct = hpke.seal_auth_psk(pkR, info, aad, pt, psk, psk_id, skS)
pt = hpke.open_auth_psk(enc, skR, info, aad, ct, psk, psk_id, pkS)
Context APIs
Context APIs allow encrypting/decrypting multiple messages with the same context:
# Setup contexts
enc, ctx_s = hpke.setup.setup_base_sender(pkR, info)
ctx_r = hpke.setup.setup_base_recipient(enc, skR, info)
# Encrypt/decrypt multiple messages
ct1 = ctx_s.seal(aad, pt1)
ct2 = ctx_s.seal(aad, pt2)
pt1 = ctx_r.open(aad, ct1)
pt2 = ctx_r.open(aad, ct2)
# Export secrets
secret = ctx_s.export(exporter_context, length)
Available Setup Methods
# Base mode
enc, ctx_s = hpke.setup.setup_base_sender(pkR, info)
ctx_r = hpke.setup.setup_base_recipient(enc, skR, info)
# PSK mode
enc, ctx_s = hpke.setup.setup_psk_sender(pkR, info, psk, psk_id)
ctx_r = hpke.setup.setup_psk_recipient(enc, skR, info, psk, psk_id)
# Auth mode
enc, ctx_s = hpke.setup.setup_auth_sender(pkR, info, skS)
ctx_r = hpke.setup.setup_auth_recipient(enc, skR, info, pkS)
# AuthPSK mode
enc, ctx_s = hpke.setup.setup_auth_psk_sender(pkR, info, psk, psk_id, skS)
ctx_r = hpke.setup.setup_auth_psk_recipient(enc, skR, info, psk, psk_id, pkS)
Context Methods
# Seal (encrypt) - only available on sender contexts
ciphertext = ctx_sender.seal(aad, plaintext)
# Open (decrypt) - only available on recipient contexts
plaintext = ctx_recipient.open(aad, ciphertext)
# Export secret - available on all contexts
secret = ctx.export(exporter_context, length)
Single-Shot Export APIs
For export-only operations without encryption:
from rfc9180.single_shot import HPKESingleShot
single = HPKESingleShot(hpke.setup)
# Base mode export
enc, secret = single.send_export_base(pkR, info, exporter_context, length)
secret = single.receive_export_base(enc, skR, info, exporter_context, length)
# PSK mode export
enc, secret = single.send_export_psk(pkR, info, exporter_context, length, psk, psk_id)
secret = single.receive_export_psk(enc, skR, info, exporter_context, length, psk, psk_id)
# Auth mode export
enc, secret = single.send_export_auth(pkR, info, exporter_context, length, skS)
secret = single.receive_export_auth(enc, skR, info, exporter_context, length, pkS)
# AuthPSK mode export
enc, secret = single.send_export_auth_psk(pkR, info, exporter_context, length, psk, psk_id, skS)
secret = single.receive_export_auth_psk(enc, skR, info, exporter_context, length, psk, psk_id, pkS)
Message Encoding Helpers
Applications often need a wire format. This library provides simple helpers for a self-describing header:
from rfc9180 import append_header, parse_header
from rfc9180.constants import HPKEMode
# Encode message with header
enc, ct = hpke.seal_base(pkR, info, aad, pt)
msg = append_header(
enc, ct,
int(hpke.kem_id),
int(hpke.kdf_id),
int(hpke.aead_id),
int(HPKEMode.MODE_BASE)
)
# Parse message header
kem_id2, kdf_id2, aead_id2, mode2, enc2, ct2 = parse_header(
msg,
enc_len=hpke.kem.Nenc
)
pt2 = hpke.open_base(enc2, skR, info, aad, ct2)
The header format is: "HPKE" | kem_id(2) | kdf_id(2) | aead_id(2) | mode(1) | enc | ct
Note: This is an application-level format following RFC 9180 §10 guidance; applications may define alternative encodings.
Constants
from rfc9180 import KEMID, KDFID, AEADID
from rfc9180.constants import HPKEMode
# KEM IDs
KEMID.DHKEM_P256_HKDF_SHA256 # 0x0010
KEMID.DHKEM_P384_HKDF_SHA384 # 0x0011
KEMID.DHKEM_P521_HKDF_SHA512 # 0x0012
KEMID.DHKEM_X25519_HKDF_SHA256 # 0x0020
KEMID.DHKEM_X448_HKDF_SHA512 # 0x0021
# KDF IDs
KDFID.HKDF_SHA256 # 0x0001
KDFID.HKDF_SHA384 # 0x0002
KDFID.HKDF_SHA512 # 0x0003
# AEAD IDs
AEADID.AES_128_GCM # 0x0001
AEADID.AES_256_GCM # 0x0002
AEADID.CHACHA20_POLY1305 # 0x0003
AEADID.EXPORT_ONLY # 0xFFFF
# HPKE Modes
HPKEMode.MODE_BASE # 0x00
HPKEMode.MODE_PSK # 0x01
HPKEMode.MODE_AUTH # 0x02
HPKEMode.MODE_AUTH_PSK # 0x03
Testing
The library includes comprehensive unit tests and support for RFC 9180 test vectors:
# Install development dependencies
pip install -e ".[dev]"
# Run all tests
pytest tests/
# Run specific test suites
pytest tests/test_kem_x25519.py
pytest tests/test_kem_p256.py
pytest tests/test_rfc9180_vectors.py
pytest tests/test_integration.py
KEM/KDF Decoupling
Per RFC 9180 §7.1, each KEM mandates a specific hash/KDF. This implementation decouples the KEM's internal KDF from the ciphersuite KDF used in the key schedule (§5.1): KEMs self-select their mandated hash; the suite KDF (e.g., HKDF-SHA256) is used for key schedule and exports.
Security Considerations
- PSK Entropy: PSKs must have at least 32 bytes of entropy (enforced)
- Key Reuse: Ephemeral keys are generated fresh for each encryption
- Nonce Management: Sequence numbers are automatically managed with overflow protection
- Validation: All inputs are validated per RFC 9180 requirements
- Key Serialization: Keys can be serialized/deserialized for storage and transmission
- Context Limits: Sequence numbers are bounded to prevent nonce reuse
Error Handling
The library provides comprehensive error handling:
from rfc9180.exceptions import (
OpenError, # Decryption failure
MessageLimitReachedError, # Sequence number overflow
)
try:
pt = hpke.open_base(enc, skR, info, aad, ct)
except OpenError:
# Decryption failed (wrong key, corrupted data, etc.)
pass
References
- RFC 9180: Hybrid Public Key Encryption
- RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- RFC 7748: Elliptic Curves for Security
License
This project is licensed under the MIT License - see the LICENSE file for details.
Contributing
Contributions are welcome! Areas for improvement:
- Performance optimizations
- Additional test vectors
- Documentation improvements
- Additional KEM/KDF/AEAD algorithm support
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rfc9180-0.2.0.tar.gz.
File metadata
- Download URL: rfc9180-0.2.0.tar.gz
- Upload date:
- Size: 3.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7fb58c684e13e645b7333148193c05878d4b63cb19b7d526428c2195f6e56213
|
|
| MD5 |
2deaed1d31994443dda5e7d2b3385b30
|
|
| BLAKE2b-256 |
cf6e5e36975a1b583858b7d26826d0fb04baa5b2d9876f2f35490be414d4b388
|
File details
Details for the file rfc9180-0.2.0-py3-none-any.whl.
File metadata
- Download URL: rfc9180-0.2.0-py3-none-any.whl
- Upload date:
- Size: 25.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7cb255f618d9d146a9edb747abd80ba7ec9b2b794743af214762a055817c10e
|
|
| MD5 |
ed3f12e71715828866674d2f523d2874
|
|
| BLAKE2b-256 |
e1a8d4f4881b21910eae1f6439ffaca8e7984966f2077265d5c6100df24e34d8
|