Seal a private key to a recipient, prove it's the right one without revealing it, and optionally require an authorizer's consent before it can be unsealed.
Lock a private key, or any secp256k1 scalar, to a recipient. What you get back is a capsule: opaque bytes that give nothing away. Store it, copy it, hand it around. It stays shut until the recipient opens it.
let capsule = Capsule::builder(&private_key, &recipient, &ctx).seal()?; let bytes = capsule.to_canonical_bytes(); // just bytes; keep them anywhere
The recipient is a public key. Sealing needs only that; opening needs the private key behind it.
ctx ties the capsule to your application, so it can't be lifted and replayed somewhere else.
Give someone the capsule and a public key, and they can confirm it really holds that key's private half. They open nothing and learn nothing.
// anyone holding the capsule and the public key can run this, no secret needed: capsule.verify_ungated(&public_key, &recipient, &ctx)?; // proven: the capsule holds the private key for `public_key`
When a distributed key generation produces a key as shares that add up to the whole, seal each share and bundle the capsules into a Case: one canonical artifact to store, replicate, or hand to a backup. Verifying the Case confirms, in a single step, that every share is present and that together they reconstitute the whole key.
let case = Case::new(capsules)?; let verified = case.verify(/* the whole key, the recipient, ctx */)?; // every share present, and together they reconstitute the whole key
Put the secret behind one or more authorizers. It then opens only by consent: the recipient plus every authorizer has to take part, and no one opens it alone, not even the recipient.
let capsule = Capsule::builder(&secret, &recipient, &ctx) .access_key(&authorizer) // an authorizer whose consent is required .seal()?;
Authorizers are public keys too. Gating needs only those; contributing needs the private key behind each.
To open, the recipient gathers each authorizer's contribution and unseals. The contributions are independent, and anyone can check that one is genuine without learning anything about the secret.
// each authorizer confirms the capsule, then creates its contribution: let verified = capsule.verify(&public_key, &recipient, &[authorizer], &ctx)?; let partial = verified.contribute(&authorizer_key)?; // the recipient gathers the contributions and unseals: let recovered = verified.unseal(&recipient_key, &[partial])?;
An authorizer doesn't have to be one person with one key. It can be a multi-party key from a distributed key generation, held jointly by a group so that no single member ever holds the whole. To the capsule it's just another authorizer, and the group produces its contribution together.
Technically, ve-capsule is verifiable encryption on secp256k1. Every capsule carries a short proof that it is well-formed and bound to exactly these participants, checkable by anyone, at any time, without learning the secret.
If you want the full construction and its security argument, it's written up in the specification →