Skip to main content

RFC9420

Pure Messaging Layer Security (MLS, RFC 9420) library in Python.

Documentation: Full docs (getting started, API reference, examples, architecture) are built with MkDocs and published to GitHub Pages. If the repo has Pages enabled, they are at https://MFDI-hub.github.io/PyMLS/ (or https://MFDI-hub.github.io/PyMLS/ for an org repo). To read them locally: uv sync --group docs then uv run mkdocs serve.

Overview

RFC9420 is a minimal, pragmatic implementation of the Messaging Layer Security protocol as specified in RFC 9420. The library provides a clean Python API for creating and managing MLS groups, handling cryptographic operations, and interoperating with other MLS implementations.

Status

  • ✅ Core wire types (MLSPlaintext, MLSCiphertext, Welcome, GroupInfo) implemented
  • ✅ Group state machine for Add/Update/Remove, commit create/process (RFC-aligned ordering)
  • ✅ Ratchet tree with RFC-style parent-hash validation
  • ✅ Welcome processing with full ratchet_tree extension (internal nodes included)
  • ✅ Config-driven API: GroupConfig, MLSGroup, MLSGroupSession, StagedCommit
  • ✅ RFC 9420 test vector support
  • ✅ External commit and PSK support
  • ✅ X.509 credential verification and revocation helpers

Quickstart

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey

from rfc9420 import (
    GroupConfig,
    DefaultCryptoProvider,
    MemoryStorageProvider,
    MLSGroupSession,
)
from rfc9420.messages.key_packages import KeyPackage, LeafNode
from rfc9420.messages.data_structures import Credential, Signature

crypto = DefaultCryptoProvider()
storage = MemoryStorageProvider()
config = GroupConfig(crypto_provider=crypto, storage_provider=storage)

def make_member(identity: bytes):
    kem_sk = X25519PrivateKey.generate()
    kem_pk = kem_sk.public_key()
    sig_sk = Ed25519PrivateKey.generate()
    sig_pk = sig_sk.public_key()
    cred = Credential(identity=identity, public_key=sig_pk.public_bytes_raw())
    leaf = LeafNode(
        encryption_key=kem_pk.public_bytes_raw(),
        signature_key=sig_pk.public_bytes_raw(),
        credential=cred,
        capabilities=b"",
        parent_hash=b"",
    )
    sig = sig_sk.sign(leaf.serialize())
    kp = KeyPackage(leaf, Signature(sig))
    return kp, kem_sk.private_bytes_raw(), sig_sk.private_bytes_raw()

# Creator (A): config-driven session
kp_a, kem_sk_a, sig_sk_a = make_member(b"userA")
session_a = MLSGroupSession.create_with_config(config, b"group1", kp_a)

# Joiner (B)
kp_b, kem_sk_b, sig_sk_b = make_member(b"userB")
session_a.add_member(kp_b, sig_sk_a)
commit_bytes, welcomes = session_a.commit(sig_sk_a)

session_b = MLSGroupSession.join_from_welcome_with_config(
    config, welcomes[0], kem_sk_b
)

ct = session_a.protect_application(b"hello")
sender, pt = session_b.unprotect_application(ct)
print(sender, pt)  # 0, b'hello'

Installation

This project uses pyproject.toml (PEP 621). Prefer uv for dependency management:

pipx install uv  # once
uv sync --dev

# Lint, type-check, test
uv run ruff check .
uv run mypy src
uv run pytest -q

Core Features

Group Operations

Use GroupConfig (crypto + storage + optional identity providers) and MLSGroupSession for the high-level API:

  • MLSGroupSession.create_with_config(config, group_id, key_package): Create a new group with an initial member
  • MLSGroupSession.join_from_welcome_with_config(config, welcome, hpke_private_key): Join via Welcome message
  • MLSGroupSession.deserialize_with_config(config, data): Restore a session from serialized state
  • session.add_member(key_package, signing_key), session.update_self(...), session.remove_member(...): Proposals
  • session.process_proposal(handshake_bytes, sender_leaf_index): Process a received proposal
  • session.commit(signing_key): Create a commit (staged merge + apply)
  • session.apply_commit(handshake_bytes, sender_leaf_index): Apply a received commit
  • session.protect_application(plaintext) / session.unprotect_application(ciphertext_bytes): Application data

For staged commits and direct group access, use MLSGroup and StagedCommit from rfc9420.

Ciphersuites

FC9420 supports all RFC 9420 §16.3 ciphersuites:

  • MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (0x0001) - Default
  • MLS_128_DHKEMP256_AES128GCM_SHA256_P256 (0x0002)
  • MLS_128_DHKEMX25519_CHACHAPOLY_SHA256_Ed25519 (0x0003)
  • MLS_128_DHKEMP256_CHACHAPOLY_SHA256_P256 (0x0004)
  • MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448 (0x0005)
  • MLS_256_DHKEMP521_AES256GCM_SHA512_P521 (0x0006)
  • MLS_256_DHKEMX448_CHACHAPOLY_SHA512_Ed448 (0x0007)
  • MLS_256_DHKEMP521_CHACHAPOLY_SHA512_P521 (0x0008)

Select a ciphersuite when creating a DefaultCryptoProvider:

crypto = DefaultCryptoProvider(suite_id=0x0001)  # Default

Advanced Features

Proposal-by-Reference

Proposals are cached on receipt using RFC 9420 §5.2 proposal references. Commits include proposal_refs that reference cached proposals. Commit processing validates that adds/removes match the referenced proposals.

External Commit

Groups publish an EXTERNAL_PUB extension in GroupInfo for external parties to join without a Welcome. External commits are path-less commits signed with the group's external key (no membership tag required).

# On the group side: external commit adds a member
commit, welcomes = group._inner.external_commit(key_package, kem_public_key)

# On the external party side: process external commit
group._inner.process_external_commit(commit_message)

Pre-Shared Keys (PSKs)

PSK proposals bind PSKs to commits via a PSK binder in authenticated_data. The binder is computed over a PSK preimage and the commit content. PSKs are integrated into the epoch key schedule.

# Create a PSK proposal
psk_proposal = group._inner.create_psk_proposal(psk_id, signing_key)
group.process_proposal(psk_proposal, sender_index)

# PSK binder is automatically computed and verified during commit processing
commit, welcomes = group.commit(signing_key)

PSK binder verification is strict by default. Configure via MLSGroup.set_strict_psk_binders(False) to relax verification.

Resumption PSK

Export the current resumption PSK for future group resumption:

resumption_psk = group._inner.get_resumption_psk()

Re-Initialization

Re-init allows migrating a group to a new group_id and resetting the epoch:

# Queue ReInit and create commit
commit, welcomes = group._inner.reinit_group_to(new_group_id, signing_key)

# On receipt, the group migrates to the new group_id and resets epoch
group.apply_commit(commit, sender_index)

Secret Tree Skipped-Keys Window

Out-of-order application/handshake decryption is supported via a sliding window of skipped keys. By default, a per-leaf window of 128 generations is enabled.

Configure at group creation:

from rfc9420.protocol.mls_group import MLSGroup

group = MLSGroup.create(
    group_id=b"group1",
    key_package=kp,
    crypto_provider=crypto,
    secret_tree_window_size=128  # Default
)

Older behavior (on-demand derivation without caching) is preserved for generations outside the window.

Ratchet Tree Truncation

The ratchet tree truncates immediately when the rightmost leaf (and all trailing leaves) are blank after a removal (RFC 9420 §7.7). This reduces sparse tails and keeps the tree hash consistent with RFC expectations.

Credentials

KeyPackage verification checks that the credential public key matches the leaf signature key. X.509 credential containers are supported:

from rfc9420.messages.credentials import X509Credential

# Verify X.509 certificate chain
cred = X509Credential.deserialize(cert_der)
cred.verify_chain(trust_roots)

Configure trust roots for chain validation:

group._inner.set_trust_roots([trust_root1_der, trust_root2_der])

X.509 Revocation Helpers

Revocation checks are pluggable. Batteries-included helpers are available:

from rfc9420.backends.identity.x509_revocation import check_ocsp_end_entity, check_crl
from rfc9420.backends.identity.x509_policy import X509Policy, RevocationConfig

policy = X509Policy(
    revocation=RevocationConfig(
        enable_ocsp=True,
        enable_crl=True,
        # Use default fetchers (best-effort HTTP). For offline, provide custom fetchers.
        ocsp_checker=lambda cert_der: check_ocsp_end_entity(cert_der, issuer_der),
        crl_checker=lambda cert_der: check_crl(cert_der, issuer_der),
    )
)

By default, helpers fail-closed on network/responder errors (return revoked). To opt into fail-open behavior, pass fail_open=True to the helper functions.

RFC 9420 Compliance

RFC9420 aligns closely with RFC 9420 semantics:

  • Update Path Derivation: Internode keys use top-down path_secret construction (RFC §7.4). A fresh path_secret is generated at the leaf and ratcheted upward with DeriveSecret(..., "path"). Each node key pair is deterministically derived from DeriveSecret(path_secret, "node").

  • Deterministic KEM DeriveKeyPair: Implements deterministic derivation for DHKEM X25519/X448 (with clamping) and for P-256/P-521 via modular reduction.

  • Proposal Ordering: Commits partition and order proposals according to RFC §12.3:

    1. GroupContextExtensions
    2. Update
    3. Remove
    4. Add
    5. PreSharedKey

    ReInit proposals remain exclusive.

  • PSK Handling: PSK proposals are bound via a commit binder and integrated into the epoch key schedule when present.

  • Transcript Bootstrap: The interim transcript hash is initialized at group creation using an all-zero confirmation tag per RFC §11.

  • Parent Hash Validation: Ratchet tree nodes include parent hash validation per RFC §7.9.

Interop & Testing

RFC 9420 Test Vectors

Run the RFC 9420 test vectors:

python -m rfc9420.interop.test_vectors_runner /path/to/vectors --suite 0x0001

Supported types include key_schedule, tree_math, secret_tree, message_protection, welcome_groupinfo, tree_operations, messages, and encryption. A JSON summary is printed.

Interop CLI (RFC Wire)

The interop CLI exposes RFC-wire encode/decode helpers for handshake and application messages:

# Encode handshake (hex → base64 TLS presentation bytes)
python -m rfc9420.interop.cli wire encode-handshake <hex_plaintext>

# Decode handshake (base64 → hex)
python -m rfc9420.interop.cli wire decode-handshake <b64_wire>

# Encode application (hex → base64)
python -m rfc9420.interop.cli wire encode-application <hex_ciphertext>

# Decode application (base64 → hex)
python -m rfc9420.interop.cli wire decode-application <b64_wire>

This is intended to interoperate with other MLS implementations (e.g., OpenMLS). Wire helpers use the TLS presentation bytes described in RFC 9420 (§6–§7 for handshake, §9 for application).

API Reference

rfc9420.Group

High-level wrapper around MLSGroup providing an ergonomic API.

Methods:

  • create(group_id: bytes, key_package: KeyPackage, crypto: CryptoProvider) -> Group
  • join_from_welcome(welcome: Welcome, hpke_private_key: bytes, crypto: CryptoProvider) -> Group
  • add(key_package: KeyPackage, signing_key: bytes) -> MLSPlaintext
  • update(leaf_node: LeafNode, signing_key: bytes) -> MLSPlaintext
  • remove(removed_index: int, signing_key: bytes) -> MLSPlaintext
  • process_proposal(message: MLSPlaintext, sender_leaf_index: int) -> None
  • commit(signing_key: bytes) -> tuple[MLSPlaintext, list[Welcome]]
  • apply_commit(message: MLSPlaintext, sender_leaf_index: int) -> None
  • protect(application_data: bytes) -> MLSCiphertext
  • unprotect(message: MLSCiphertext) -> tuple[int, bytes]

Properties:

  • epoch: int - Current group epoch
  • group_id: bytes - Group identifier

rfc9420.DefaultCryptoProvider

Concrete CryptoProvider implementation using the cryptography library.

Constructor:

  • DefaultCryptoProvider(suite_id: int = 0x0001)

Properties:

  • supported_ciphersuites - List of supported RFC suite ids
  • active_ciphersuite - Currently selected MlsCiphersuite

Methods:

  • set_ciphersuite(suite_id: int) -> None - Select a different ciphersuite

rfc9420.protocol.mls_group.MLSGroup

Core protocol implementation. Most users should use Group instead.

Advanced Methods:

  • external_commit(key_package: KeyPackage, kem_public_key: bytes) -> tuple[MLSPlaintext, list[Welcome]]
  • process_external_commit(message: MLSPlaintext) -> None
  • create_psk_proposal(psk_id: bytes, signing_key: bytes) -> MLSPlaintext
  • reinit_group_to(new_group_id: bytes, signing_key: bytes) -> tuple[MLSPlaintext, list[Welcome]]
  • get_resumption_psk() -> bytes
  • set_strict_psk_binders(strict: bool) -> None
  • set_trust_roots(trust_roots: list[bytes]) -> None

Breaking Changes

Update Path Derivation: Internode keys on an update path are now derived using a top-down path_secret construction (RFC §7.4). A single fresh path_secret is generated at the leaf and ratcheted upward with DeriveSecret(..., "path"). Each node key pair is deterministically derived from DeriveSecret(path_secret, "node"). This replaces the previous strategy of generating fresh key pairs and extracting secrets from private keys.

Migration Guidance: Groups created with previous versions that expect the legacy update-path derivation are not wire-compatible. Recreate groups or re-onboard members using new Welcomes.

Enhancements

  • Deterministic KEM DeriveKeyPair for all supported KEMs
  • RFC §12.3 proposal ordering
  • PSK binder integration
  • Transcript bootstrap with zero confirmation tag
  • Secret tree skipped-keys window (default: 128)
  • Ratchet tree truncation per RFC §7.7

Notes

  • The library aims for correctness and clarity first. Recent updates removed MVP shortcuts and aligned with RFC 9420 semantics for group creation, commit ordering, parent hash, and Welcome tree encoding.
  • External HPKE backend is provided by cryptography (fails fast if unavailable).
  • Revocation helpers default to fail-closed unless explicitly configured to fail-open.
  • The legacy DAVE protocol and opcodes were removed. This is a pure MLS library now.
  • GroupContextExtensions proposals are accepted and ordered. Their data is merged into GroupInfo extensions for Welcomes; the GroupContext structure in this codebase remains minimal and does not store extensions explicitly.

License

[Add your license information here]

Contributing

[Add contributing guidelines here]

Release files for rfc9420 1.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 rfc9420 1.3.0
File Size Uploaded
rfc9420-1.3.0.tar.gz 211.4 kB Details

Built distribution (wheel)

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

Total release size: 365.7 kB

Release files / rfc9420-1.3.0.tar.gz

Download URL rfc9420-1.3.0.tar.gz
Size 211.4 kB
Tags Source
SHA-256 checksum
How to use checksums
8e6c5c2a35e4c479080a2a90d8cb44a9440d62b458cd69d6d03154c0fc1c66b4
BLAKE2b-256 checksum
How to use checksums
14311fe7fd6de8edc2d60d452af43c69e16cb7c811f91f0ea65715b03075f825
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.11

Release files / rfc9420-1.3.0-py3-none-any.whl

Download URL rfc9420-1.3.0-py3-none-any.whl
Size 154.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
eb82b1f53d8e4b20292c384614109e8b7669b22cdf8968f34db0efc6518f28e9
BLAKE2b-256 checksum
How to use checksums
4a2f77f8798a63a74a0d57e5ebf1e54aa94bead9314c2e91d6b24a074a218032
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.11

Release history Release notifications | RSS feed

This release

1.3.0 This release

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

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