Docs
Sealed mode
Sealed mode makes the "we cannot see your signals" guarantee structural rather than contractual: payloads are end-to-end encrypted client-side by the Python SDK, and the relay stores, sequences, and fans out ciphertext it cannot open. It is a per-strategy option, chosen at creation ("sealed": true) and immutable after. Everything else — sequencing, catch-up, retries, the delivery ledger — works unchanged, because none of it ever needed to read your payload.
Threat model
What sealed mode defends against, and — stated just as plainly — what it does not. The relay sees envelope metadata only: IDs, sequence numbers, timestamps, envelope size, recipient count.
| Scenario | Outcome |
|---|---|
| Relay compromise or malicious insider | Sees ciphertext plus envelope metadata only. Cannot decrypt (holds no private keys) and cannot forge (signatures are the publisher's). |
| Relay tampers with key distribution | Detected: the SDK recomputes every key-bundle fingerprint locally, and pinned fingerprints fail closed on mismatch. The relay is untrusted even as a keyserver. |
| Signature stripping / downgrade | Rejected: sealed signals carry dual Ed25519 + ML-DSA-65 signatures, and a delivery missing either is refused by the SDK. |
| Harvest-now-decrypt-later (future quantum adversary) | Defeated while either component holds: key wrap is hybrid X25519 + ML-KEM-768 (NIST FIPS 203) via HKDF-SHA256 with transcript binding. |
| Traffic analysis | Visible by design: who publishes, who receives, when, and how big. Sealed mode hides content, not existence or timing. |
| Compromised subscriber endpoint | Out of scope: a party entitled to decrypt can leak what it decrypted. Delivery traceability (per-seat HMAC, ledger) still attributes the seat. |
Wire format — sealed envelope v1
Each signal is encrypted under its own fresh 256-bit key with ChaCha20-Poly1305; that key is wrapped once per entitled recipient with hybrid X25519 + ML-KEM-768 (HKDF-SHA256, transcript-bound), and the whole envelope carries dual Ed25519 + ML-DSA-65 publisher signatures. The sketch below shows the shape and the algorithm identifiers — the normative schema is in the API reference.
{
"sealed": {
"v": 1,
"alg": {
"aead": "chacha20poly1305",
"kem": "x25519-mlkem768",
"kdf": "hkdf-sha256",
"sig": "ed25519+mldsa65"
},
"recipients": [
{ "key_id": "sk_9f2c", "wrapped_key": "<base64>" }
],
"nonce": "<base64>",
"ciphertext": "<base64>",
"sig": { "ed25519": "<base64>", "mldsa65": "<base64>" }
}
} 1 · Install
The cryptography ships as an SDK extra — the core package stays dependency-light for tenants who do not use sealed strategies.
$ pip install "multiedge-relay[sealed]" # SDK >= 0.4.0 2 · Generate and register keys
Every subscriber generates a recipient bundle; the publisher generates a sender bundle. Only the public halves are registered with the relay (POST /v1/clients/{id}/sealed-keys, PUT /v1/strategies/{id}/sealed-keys/sender) — private keys never leave your machines, and the relay could not use them if they did: it performs no cryptographic operations on payloads at all. Rotation is explicit: register a new bundle, then revoke the old one (DELETE /v1/sealed-keys/{keyId}).
$ multiedge sealed keygen --kind recipient --out subscriber-key.json
$ multiedge sealed keygen --kind sender --out publisher-key.json
# Register the public bundles with the relay (private halves never leave disk):
$ multiedge sealed register --help # per-client recipient bundles, per-strategy sender bundle 3 · Verify fingerprints out-of-band
The relay distributes public key bundles, but it is untrusted even for that: the SDK recomputes each bundle's fingerprint locally from the key material, and pinned_recipients / pinned_sender fail closed on any mismatch. Fingerprints display grouped in blocks of four characters (K7QX 2MRD 9WNL 04BT …) precisely so two humans can read them to each other. Confirm them over a channel the relay does not control — a phone call, a signed email, in person — before pinning. A pinned fingerprint is what turns "the relay says this is your counterparty's key" into "we verified it ourselves".
4 · Publish sealed
Attach a Sealer to the publisher. Sealing happens before the request leaves your process — a fresh key per signal, wrapped for each pinned recipient, dual-signed. Everything else about publishing (idempotency, the gapless sequence, the ack) is unchanged.
from multiedge_relay import Signal, SignalPublisher
from multiedge_relay.sealed import Sealer
sealer = Sealer.from_relay(
api_key="mesk_...",
strategy_id="str_dm_rotation",
pinned_recipients={ # fingerprints verified out-of-band (see below)
"cli_9f2c": "K7QX 2MRD 9WNL 04BT ...",
},
)
relay = SignalPublisher(api_key="mesk_...", sealer=sealer)
ack = relay.publish(Signal(strategy_id="str_dm_rotation",
client_signal_id="2026-08-14-close",
payload=rebalance))
# The payload is sealed BEFORE the HTTP request leaves your process.
# The relay sequences, stores, and fans out ciphertext it cannot open. 5 · Subscribe and unseal
Attach an Unsealer to the subscriber. The SDK verifies both publisher signatures — rejecting a delivery with either one stripped as a downgrade — then decrypts, and hands your handler plaintext. Cursor catch-up, replay, and exactly-once processing work identically on sealed strategies; the relay replays ciphertext.
from multiedge_relay import SignalSubscriber
from multiedge_relay.sealed import Unsealer
unsealer = Unsealer.from_relay(
api_key="mesk_...",
strategy_id="str_dm_rotation",
pinned_sender="V3JH 8KPF 61QZ TX0M ...", # publisher fingerprint, out-of-band
)
def handle(signal, meta):
store(signal) # plaintext — decrypted and signature-verified locally
sub = SignalSubscriber(api_key="mesk_...",
strategy_id="str_dm_rotation",
on_signal=handle,
unsealer=unsealer)
sub.run() # dual Ed25519 + ML-DSA-65 verification, then ChaCha20-Poly1305 open 6 · Webhooks
Webhook consumers pass the same unsealer to the verification call. The per-endpoint HMAC transport signature is an unchanged, separate layer — it authenticates the delivery over the exact ciphertext bytes sent; the sealed layer inside it authenticates and decrypts the payload.
from multiedge_relay import verify_signature
signal = verify_signature(..., unsealer=unsealer)
# Same call as the HMAC-only flow, plus the unsealer: the transport HMAC is
# checked first (unchanged, separate layer), then the payload is unsealed
# locally. Dead-lettered deliveries store ciphertext — nothing in the relay's
# retry or DLQ path ever holds plaintext. Limits, stated honestly
- Up to ~100 entitled recipients per sealed strategy: the per-recipient key wraps must fit the 256 KiB envelope cap. An increase is on the roadmap.
- Late entitlements cannot decrypt history. A subscriber entitled after a signal was sealed was not among its wrapped recipients, and there is no re-encryption — the relay could not re-encrypt if it wanted to.
- Metadata and traffic timing remain visible by design: strategy, sequence, timestamps, size, recipient count.
- Plaintext features are structurally unavailable: field-level entitlement redaction and the forbidden-term compliance scan require plaintext, so the API rejects those combinations loudly — a sealed strategy cannot have a compliance_profile.
- Sealed is immutable per strategy: a strategy is created sealed or not, and never converts. Dead-lettered sealed deliveries store ciphertext, like everything else on the relay.
API reference
The sealed-key registry routes — register, list, rotate, revoke — and "sealed": true on strategy creation, documented in the OpenAPI reference.