Skip to main content

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

With uv (recommended):

uv pip install rfc9180

With pip:

pip install rfc9180

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 (uv)
uv sync --extra dev

# Run all tests
uv run pytest tests/

# Run specific test suites
uv run pytest tests/test_kem_x25519.py
uv run pytest tests/test_kem_p256.py
uv run pytest tests/test_rfc9180_vectors.py
uv run 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

Versioning

Use scripts/update_version.py to bump the version. It updates pyproject.toml and src/rfc9180/__init__.py (__version__); do not edit those version strings manually. Docs read the version from pyproject.toml.

uv run python scripts/update_version.py 0.2.1
uv run python scripts/update_version.py 0.3.0 --dry-run

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

Release files for rfc9180 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rfc9180 0.3.0
File Size Uploaded
rfc9180-0.3.0.tar.gz 3.6 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for rfc9180 0.3.0
File Interpreter ABI Platform
rfc9180-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 3.7 MB

Release files / rfc9180-0.3.0.tar.gz

Download URL rfc9180-0.3.0.tar.gz
Size 3.6 MB
Tags Source
SHA-256 checksum
How to use checksums
bfa75211cebbb70c94a6266a7946caf9e463f3def44abc331e1436dabf7d8b8b
BLAKE2b-256 checksum
How to use checksums
475a2a46e315408ecb3a032369d11310281c9a04bf4a9ad95f02a232200d9cce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.11

Release files / rfc9180-0.3.0-py3-none-any.whl

Download URL rfc9180-0.3.0-py3-none-any.whl
Size 29.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b4a76ec8ae9036d15dc3ed03e1cd24a0ebbb47f154400392893bf555c6a148fb
BLAKE2b-256 checksum
How to use checksums
c136791a30acbcf7fee99b7b18adc6cabba21723475e0a452f8a87ddd42a550a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.11

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page