ve-capsule · verifiable encryption on secp256k1

Recovery shares. Proven together.

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.

secp256k1 · segmented EC-ElGamal · Bulletproofs++ · compact recovery payloads · no unsafe
01 · seal

Each party encrypts its share to the recovery public key. Optionally behind consent gates.

02 · verify

Everyone else can check the claim. Nothing used here opens the capsule or gives a decryption key.

03 · open

The recipient secret — plus any required gate contributions — recovers the share later.

Where it helps

When more than one party cares.

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.

  • Threshold recovery

    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.

  • Offline recipient

    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.

  • Consent-gated recovery

    Require one or more access keys (or a threshold key no single person holds) to contribute before the recipient can open.

  • Self-audit & cold storage

    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.

Artifact

Opaque bytes. Public check.

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.

01Seal

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

02Verify

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.

03Open

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.

Consent

Gate recovery behind consent.

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.

Anatomy

A Case is capsules.

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.

a case · three piece-capsules 3 × 5,414 B
piece 0M₀
piece 1M₁
start end
piece 2M₂

verify: M₀ + M₁ + M₂ = M · shared (recipient, gates, ctx)

one capsule on the wire 5,414 bytes
  • Envelope Magic ve-capsule.cap.v1, version byte, and the commitment C = m·G (here M₁) — the public claim the rest of the proof is about.
    51 B18 + 33
  • Segmented EC-ElGamal Eleven limb ciphertexts (Ek, Dk). This is the only part recovery needs to open — the rest is for auditors.
    726 B22 × 33
  • Pedersen limb commitments Value and complement commitments per limb (Comk, Com̄k) — the range statement’s public inputs.
    726 B22 × 33
  • Bulletproofs++ One aggregated reciprocal range proof over every limb and carry bit: four commitments, six fold rounds, five residual scalars. Logarithmic in the statement — not one proof per limb.
    688 B16×33 + 5×32
  • Carry chain Proves the limb decomposition is exact integer arithmetic m + m̄ = n − 1, so a verifying capsule is a decryptable one.
    1,045 B21×33 + 11×32
  • Linking sigma Binds the ElGamal limbs, the Pedersen commitments, and C under one Fiat–Shamir challenge — so the range proof is about the same scalar the ciphertext encrypts.
    2,178 B34×33 + 33×32

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.

Compact

Proof for auditors. Recovery stays small.

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.

≈5.4 KB / proof 65 bytes a piece Recipient-only: one ECDH, one hash, and one subtraction per piece — no proof code in the opener. The proof never covers a hint; recovery re-anchors on the certified target 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
One curve

The curve you already speak.

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.

Building on others

BP++ for range. The crate for the rest.

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.

Under the hood

Segmented EC-ElGamal.

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 →