Ciphertext alone is a promise. When several parties each seal a piece of a key, hoping the others sealed the right thing is not a security model.
ve-capsule attaches a public proof to each seal. Anyone can check another’s capsule
encrypts the scalar behind the claimed share M = m·G — without opening
it, and without the recipient online.
Nothing leaks beyond that already-public discrete-log instance. Lying (or honest mistakes) fails at setup, in front of everyone with skin in the game.
Each party encrypts its share to the recovery public key. Optionally behind consent gates.
Everyone else can check the claim. Nothing used here opens the capsule or gives a decryption key.
The recipient secret — plus any required gate contributions — recovers the share later.
The pattern is multi-party: several devices or people each contribute sealed material, and everyone who will live with the result needs to know the others did not seal garbage, withhold a real share, or “helpfully” substitute a different key. Verifiable encryption makes that check public and offline-friendly. Catching your own mistakes is a free side effect.
Each participant seals their piece toward the same recipient. The group verifies the Case so the pieces add up to the certified public key before anyone walks away.
The recovery device does not need to be online for setup. Peers still confirm every seal against the expected public shares before relying on the package.
Require one or more access keys (or a threshold key no single person holds) to contribute before the recipient can open.
You can also re-verify your own backups without decrypting them, and after group verification drop the multi-kilobyte proofs for a small recipient-only payload.
Ordinary encryption is a promise between sealer and recipient. In a multi-party setup that is
not enough: the other signers, the recovery coordinator, anyone who will depend on the artifact
later needs the same guarantee you do. A capsule is still storage-friendly bytes — a few
kilobytes you can hand around — but its envelope exposes the public claim C = m·G
and its proof binds that claim to the ElGamal limbs. Peers check that at seal time, while the
recovery device may still be offline. A wrong or empty share fails in front of the room, not
months later in a crisis.
Needs the issued share, the recovery public key, and context bytes. What comes back is canonical bytes — keep them wherever you keep other blobs.
let capsule = Capsule::builder(&secret, &recipient, &ctx).seal()?; let bytes = capsule.to_canonical_bytes(); // just bytes; store them anywhere
Needs the capsule and public keys only. A passing check means the encrypted limbs
reassemble to a scalar whose commitment is public_key. Nothing in this call can open
the capsule.
// no secret anywhere in this call capsule.verify_ungated(&public_key, &recipient, &ctx)?; // proven: limbs reassemble to the dlog of public_key (= M). still sealed.
Needs the recipient secret. Verify first, then recover. Opening re-checks that the recovered scalar matches the expected public key and fails closed otherwise.
let capsule = Capsule::from_canonical_bytes(&bytes)?; let verified = capsule.verify_ungated(&public_key, &recipient, &ctx)?; let secret = verified.unseal(&recipient_secret, &[])?;
ctx binds the capsule to your application transcript — package id,
epoch, purpose — so the same bytes cannot be lifted and replayed somewhere else.
A gate is another public key that has to contribute before unseal succeeds. With no gates, the recipient opens alone. With gates, every named key takes part — and a gate can itself be a multi-party key, so no single person holds the whole authorizer scalar. To the capsule that is just another access key; the group produces its contribution together.
let capsule = Capsule::builder(&secret, &recipient, &ctx) .access_keys(&quorum) .seal()?; // unsealing now needs their contributions too
After verification you can strip a capsule down to a compact opening core. For gated recovery, a quorum signature over the canonical attestation statement — core digest, recipient, gate roster, context, and params — stands in for the original proof. The built-in scheme is BIP-340 Schnorr; FROST(secp256k1)-TR signatures verify as BIP-340 under the caller-supplied x-only key (Taproot key-path users supply the tweaked output key). The signature is there for a concrete reason: a stripped core has no proof left, so without an attestation an attacker could hand an authorizer a fabricated core and harvest a partial decryption (a static-DH oracle on their key). Recipient-only recovery needs no signature; nobody contributes, so there is no oracle to open.
When a secret is already additive pieces — the usual shape for threshold and MPC recovery —
each party seals its own piece. A Case is that bundle: same recipient, same access
policy, same context; different piece commitments Mⱼ. The group check is one
equation, Σ Mⱼ == M. Below: three piece-capsules in a Case; brackets mark where
one capsule starts and stops; the wire breakdown under the arrow is that capsule, expanded.
verify: M₀ + M₁ + M₂ = M · shared (recipient, gates, ctx)
ve-capsule.cap.v1, version byte, and the commitment
C = m·G (here M₁) — the public claim the rest of
the proof is about.
(Ek, Dk).
This is the only part recovery needs to open — the rest is for auditors.
Comk, Com̄k) — the range statement’s
public inputs.
m + m̄ = n − 1, so a verifying capsule is a
decryptable one.
C under one Fiat–Shamir challenge — so the range proof is about
the same scalar the ciphertext encrypts.
After verify, most of this can leave. Strip drops everything but the
core envelope and ElGamal limbs → 778-byte opening core per piece: the same
ciphertexts the group verified, and the only compact form that keeps gates. Beside it, a
separately sealed 65-byte recovery hint per piece opens with almost no computation
at all. Both re-anchor on certified M.
When a scalar is already additive pieces — the natural shape for many threshold and MPC
setups — seal each piece and bundle the capsules into a Case. One check,
Σ Mⱼ == M, proves the pieces add up to the expected public key. After that
audit, the stripped core is the default compact form: the verified ciphertexts themselves,
gates intact. The recovery hint exists for a different opener entirely — hardware with just
enough silicon to hold a key and do one ECDH, nowhere near a bounded-dlog search. A hint is
a 65-byte ECDH-openable shadow of one contribution, sealed beside the capsule with no proof
attached; gathered into one recipient-only payload, the multi-kilobyte proofs stay behind.
If you need interpolation weights, apply them before sealing.
M, so a bad payload fails closed — it can refuse to yield
the secret, never yield a wrong one that looks right. Keep the cores when you want the
audited fallback.
let rctx = RecoveryContext { certified_target: &expected_public_key, ctx: &context_bytes, epoch, }; // producers seal hints beside their capsules; the payload carries no proofs let hint = seal_recovery_hint(&contribution, &recipient, &rctx, idx, &mut rng)?; let payload_bytes = assemble_recipient_recovery_payload(&expected_public_key, &hints)?; // no proof bytes
Recipient keys, gates, recovered secrets, and quorum signatures all live on secp256k1. No class group, no pairing curve, no RSA modulus. That is a practical choice as much as a cryptographic one: Bitcoin, FROST, and Taproot systems already hold these keys and signers, so a gated recovery can be authorized with an ordinary secp256k1 signature instead of standing up a second algebraic setting just for the recovery path.
The proof says a capsule targets the supplied recipient and gate keys; it does not say those keys are enrolled or policy-authorized. The crate rejects identity, publicly-enumerable, and bounded-relation key inputs, but possession and enrollment binding remain integration obligations.
A range proof can show committed numbers sit in a window. It does not decide who can open a ciphertext, how recovery re-checks itself, how approvers contribute without becoming oracles, or how a multi-kilobyte proof becomes a small cold-storage payload. Those are the seams applications actually need.
ve-capsule sits in the Camenisch–Shoup verifiable-encryption tradition, adapted to segmented EC-ElGamal on native secp256k1; its closest prior instantiation is ZenGo's Juggling, which sealed a secp256k1 scalar the same way — ve-capsule is a hardened re-instantiation of that construction. The range core is the reciprocal argument of Eagen, Kanjalkar, Ruffing, and Nick (EUROCRYPT 2024), implemented from the paper with corrections from Cypher Stack's review — which corrected specific lemmas but did not certify the argument overall. Around that: context binding, recipient opening, consent gates, quorum signatures for stripped cores, Cases for additive pieces, and recovery hints. The proof algebra stays crate-private so callers cannot reach past the consent-gated surface.
The scalar is encrypted in small segments. One aggregated
Bulletproofs++ argument covers every segment and carry at once — logarithmic
in the statement, not linear — with a linking sigma tying ciphertexts to the committed key.
The seal proof is non-interactive on one Fiat–Shamir transcript. Each authorizer
Partial carries its own non-interactive DLEQ transcript (separate from the seal
proof). The proven range is exactly the decryption search window (zero slack), so a capsule
that verifies is a capsule that decrypts. Generators are nothing-up-my-sleeve points each party
re-derives locally; there is no trusted setup. Verification is mostly multiscalar equations
plus canonical decode and degenerate-key/mask screens. Secrets zeroize on drop. Sealing and
unsealing are not constant-time — run them where the secret already lives; verification only
needs public data.
The claim is knowledge-sound binding and zero-knowledge in the random-oracle
model, with secrecy relative to the public commitment C = m·G. A guessable scalar
is already checkable against that C by anyone, so do not escrow low-entropy secrets
expecting secrecy. Simulation-sound extractability and UC composition are not claimed.
The construction and security argument are in the specification →