Vault
A time-locked attestation vault: seal base64 content under AES-256-GCM and get the key back only when a reveal condition is met - a deadline passes, enough authorized signers approve, or a probabilistic reveal lottery draws a winner. The content key is Shamir-split at seal time, so the server alone cannot open a sealed vault. Every example on this page was executed against a live server.
When to reach for it: sealed-bid commitments revealed after close, embargoed announcements, contest answers published at a deadline, tournament strategy escrow until the match ends, vesting secrets released on a cliff.
When not to: secrets you want to read back at will belong to KV Store - a vault has no plain read, only condition-gated reveal. Weighted draws without sealed content belong to Lottery.
Concepts
- Attestation - one sealed record: encrypted blob, SHA-256 content
hash, a commitment to the content key, lock configuration, and immutable
audit history. Seal parameters are committed to a merkle root (the
lock_proof); there is no update or delete endpoint. - Lock types -
time_lockopens atunlock_at;multi_signatureopens when enough authorized signers have signed;hybridhas a deadline plus optional early unlock by signatures. - Split key custody - the content key is Shamir-split (threshold 2 by
default, minimum 2). The server keeps
share_threshold - 1custody shares; the rest are holder shares, returned exactly once at seal. Losing them all makes the content unrecoverable; anyone holding one can claim the reveal once the gate opens - they are bearer credentials. - Gate vs key - unlocking is two steps: the gate opens (status flips
locked->partially_unlocked->unlocked, one-way, exactly once), then the key is reconstructed from custody shares plus the holder shares in the request. A passed deadline alone reveals nothing. - Reveal lottery - optional probabilistic early reveal: entrants buy entries, each an independent deterministic draw, and a winner receives the key. Lottery vaults are the one exception to split custody: the server keeps a reconstructing share (threshold 1) so it can release the key to an anonymous winner.
Seal
snug vault seal --content-type docs4-report \
--data "$(echo -n 'the sealed bid is 4200' | base64)" \
--unlock-at 2026-08-28T04:35:22Z{
"attestation_id": "vault_att_VhrWTMcebzYSbyxmztbb",
"status": "locked",
"share_threshold": 2,
"holder_shares": ["ApH4fRH435ftCHO0bbEhm6moQI9nJshGg3S0Lgl3qU42"],
"lock_proof": { "merkle_root": "sha256:f7af1157fd2fbe...", "leaves": ["..."] },
"unlock_scheduled": "2026-08-28T04:35:22Z"
}This is the only time holder_shares is ever returned. Content must be
valid, non-empty base64 (400 invalid_content); time_lock and hybrid
require unlock_at (400 invalid_lock_config); share_threshold below 2
is rejected on non-lottery vaults.
Status, list, history
snug vault status -a vault_att_VhrWTMcebzYSbyxmztbb
snug vault list --status locked --page-size 5
snug vault history -a vault_att_VhrWTMcebzYSbyxmztbbOver HTTP, GET /api/v1/vault/attestations/{id}/status returns the
enveloped lock state (captured live):
{
"status": 200,
"msg": "OK",
"data": {
"attestation_id": "vault_att_EesxXTBrFFztjVaRYAxq",
"status": "locked",
"owner": "docs-wave",
"lock_type": "time_lock",
"unlock_at": "2030-01-01T00:00:00Z",
"time_remaining_seconds": 105564169,
"unlock_progress": { "signatures_collected": 0, "signatures_required": 0, "signers": [] }
}
}Status is visible to the owner, a platform admin, or an authorized signer;
anyone else gets 403 insufficient_permissions. History (the created /
witness_added / unlocked audit events) is owner or admin only. list
returns only your own attestations, newest first.
Time-lock reveal
Before the deadline, unlock is refused - reproduced live:
403 still_time_locked, "Attestation is still time-locked until
2026-08-28T04:35:22+00:00". After the deadline, present at least one
holder share:
snug vault unlock -a vault_att_VhrWTMcebzYSbyxmztbb \
-k "ApH4fRH435ftCHO0bbEhm6moQI9nJshGg3S0Lgl3qU42"The response carries revealed.content_key (hex), revealed.sealed_blob
(base64 nonce || AES-256-GCM ciphertext), and an unlock_proof naming
witnesses, hashes, and merkle root. The server never returns plaintext -
decrypt locally; verified round trip:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
blob = base64.b64decode(sealed_blob)
AESGCM(bytes.fromhex(content_key)).decrypt(blob[:12], blob[12:], None)
# b'the sealed bid is 4200'Unlocking with too few shares fails with 403 insufficient_shares
("presented 1, requires at least 2" - the count includes the server's
custody share); nothing is consumed, retry with the right shares. An
optional background worker can flip expired vaults to unlocked on
schedule (off by default), but the key is only ever released to a caller
presenting shares.
Multi-signature and hybrid locks
Early reveal by an approval quorum - authorized signers, witness
signatures, 202 partial progress, hybrid deadlines - has its own page:
Multi-signature unlock.
Lottery reveal
A vault sealed with --lottery lets anyone in the tenant buy entries; each
entry is an independent draw at the configured probability, deterministic
per entrant and entry index, so re-submitting cannot re-roll it. Sealed
with --lottery-probability 1.0 for a demo, then entered by a non-owner:
snug vault lottery-enter -a vault_att_SxFxJarTZMnJgsFHYwst -c 1{ "entries_purchased": 1, "total_entries": 1, "revealed": true, "status": "unlocked", "content_key": "c70a024dabad3d79..." }A winner gets content_key directly; the sealed_blob comes from a
follow-up snug vault unlock, which needs no shares here because lottery
vaults are operator-custody. Entering a vault sealed without a lottery
fails with 400 lottery_not_enabled; entries are capped per user, and the
lottery closes after max_winners reveals.
Behaviors and gotchas
All reproduced live:
- Holder shares are the credential, not identity. A third user with a
valid holder share received the full reveal of an expired vault; the same
user without one got
403 insufficient_sharesand cannot even read status (403 insufficient_permissions). Sharing a share is sharing the content. unlock_typemust match the lock.multi_signatureunlock on a pure time-lock returns403 unlock_conditions_not_met; alotteryunlock type is always refused - wins are claimed through the entry endpoint.- Unlocked is not consumed. Unlocking an already-unlocked vault re-runs
key reconstruction and returns the reveal again
(
unlock_method: "already_unlocked") - useful for lottery winners and late-arriving share holders. - No delete. Attestations are permanent records; a global cap (100,000 by default) bounds the total.
Limits and configuration
Content up to 1 MiB, at most 32 authorized signers and 16 required
signatures, and 1000 lottery entries per user by default - tunable, along
with the auto-unlock worker, via the VAULT_* variables in the
Vault CONFIG reference.
Reference
- Vault API - every endpoint, callable
- Related: Lottery for weighted draws, Attestation for account-ownership proof, KV Store for plain storage