Skip to main content

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

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rfc9180-0.3.0.tar.gz (3.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rfc9180-0.3.0-py3-none-any.whl (29.5 kB view details)

Uploaded Python 3

File details

Details for the file rfc9180-0.3.0.tar.gz.

File metadata

  • Download URL: rfc9180-0.3.0.tar.gz
  • Upload date:
  • Size: 3.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for rfc9180-0.3.0.tar.gz
Algorithm Hash digest
SHA256 bfa75211cebbb70c94a6266a7946caf9e463f3def44abc331e1436dabf7d8b8b
MD5 6e02b0447e0ee6629dede20780018881
BLAKE2b-256 475a2a46e315408ecb3a032369d11310281c9a04bf4a9ad95f02a232200d9cce

See more details on using hashes here.

File details

Details for the file rfc9180-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: rfc9180-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 29.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for rfc9180-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b4a76ec8ae9036d15dc3ed03e1cd24a0ebbb47f154400392893bf555c6a148fb
MD5 bd0d67f8c4a4b88f6dc85e6462259205
BLAKE2b-256 c136791a30acbcf7fee99b7b18adc6cabba21723475e0a452f8a87ddd42a550a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page