Sealed-mode disclosure reference

Terminology. The canonical vocabulary on every surface is proof (grouped in folders); API responses emit the canonical names only (proof_id, proof_url, folder_slug). Where this spec shows receipt / matter / bundle_id, it is documenting a frozen on-disk/on-chain format, a stable filename (RECEIPTS.md), or a legacy route that remains accepted inbound — proof and receipt denote the same record. Full alias map: the compatibility map.

This is the sealed-mode disclosure reference: when to disclose a sealed Satsignal proof, what to share, with whom, and what the recipient does. For the product overview and a guided starting point, see the docs at https://satsignal.cloud/docs.html.

Audience: holders of a sealed Satsignal proof who need to publish, prove possession, or hand over evidence — usually attorneys, journalists, investigators, compliance teams, or anyone preserving artifacts before a controlled reveal.

This doc is operational guidance — when to disclose, what to share, with whom, and what the recipient does — alongside the cryptographic construction it relies on.


What you have, and what each piece does

When you anchor a sealed proof, you walk away with three artifacts:

  1. The original file — stays with you, never reaches Satsignal.
  2. The .mbnt bundle — a small zip containing manifest.json (with the master salt), canonical.json (with the salted commitments), and optionally proofs.json (with per-chunk leaf commitments).
  3. The on-chain transaction — public, permanent, identified by a txid. Reveals only that some file was anchored at the timestamp.

The bundle is the bearer artifact: anyone holding it can verify the seal against any candidate file. Lose it to an adversary, and the seal is broken (they can confirm "yes, you held this thing"). Lose it entirely, and the proof can no longer be verified — though the chain entry remains forever.


Scope of unseal proofs

An unseal proof shows that someone held a sealed fingerprint at a specific time AND can now reveal the underlying data that matches that commitment. The four-clause Satsignal scope statement applies in full — see What it proves, and what it doesn't.

Unsealing reveals the data; it does not retroactively grant authorship, pre-existence, or legal compliance — those remain out of scope for any Satsignal proof, sealed or not.


Three modes of disclosure

Sealed mode supports three disclosure shapes. Pick the one that matches your threat model.

Mode 1 — Full unseal (public)

What you do: publish the original file + the .mbnt bundle together. Anyone who reads the publication can verify against the public chain.

When to use:

What's revealed:

What stays private:

Operational notes:

Caveat — grindable plaintext. Mode 1 reveals the original file together with the salt. For files with high inherent entropy this is fine — there is nothing useful for an adversary to "guess." But for small or low-entropy plaintexts (a short yes/no answer, a numeric range, a name from a known shortlist), publishing the file + salt means anyone can also brute-force any related sealed commitment that might have been one of those plausible answers: they grind candidates, HMAC each one with the disclosed salt, and check against any sealed commitment they hold. The on-chain anchor still proves you held this exact file by date T (that is sealed mode's primary job); but the privacy of related sealed records that share a salt scheme degrades the moment a low-entropy Mode-1 publication lands.

If the plaintext is small or low-entropy, prefer Mode 2 or Mode 3 — they don't disclose the salt to outsiders. The same caveat applies to the chunk-row case; see /spec-merkle-row §4.1 for the HMAC-vs-SHA-only discussion.

Mode 2 — Selective disclosure (one chunk only)

What you do: publish a single page (PDF), row (CSV), key (JSON), or line (text) of the original — not the rest. Plus the per-leaf salt for that chunk and the Merkle path proving the chunk's membership in the tree.

When to use:

What's revealed:

What stays private:

Limit: each selective disclosure reveals one chunk independently. Doing it ten times reveals ten chunks (along with their ten salts) — the rest of the document still stays sealed.

Tooling note: the browser verifier handles full-bundle verification. A single-chunk selective disclosure is verified from the chunk's canonical bytes, its Merkle proof path, and — for sealed carriers only — its per-leaf salt. The leaf rule branches on the carrier algo, so a single "always HMAC the salt" recipe is wrong for the standard profile:

Then walk the proof path to the root: for each step {side, hash}, set frontier = sha256(hash + frontier) when side is "L", else frontier = sha256(frontier + hash) (+ is raw-byte concatenation). The final frontier must equal the committed chunk_merkle.root, which must equal the on-chain marker. The native (anchor-committed) tree duplicates the last node on an odd level — an unpaired last node self-pairs, so its proof step is {side:"R", hash:<its own hash>} and its parent is sha256(h + h).

Mode 3 — Private adversarial verification (legal/discovery)

What you do: hand the original file + the .mbnt bundle to a specific recipient (opposing counsel, regulator, court-appointed expert) under a non-disclosure or controlled-discovery agreement.

When to use:

What's revealed:

Operational notes:


How a recipient verifies

Whichever disclosure mode you use, the recipient's job is the same in shape: recompute the commitments locally, compare to the canonical doc, then confirm the canonical doc's hash matches the on-chain marker.

Full unseal: one-click via /verify

  1. Open https://proof.satsignal.cloud/verify (or a saved local copy of that page).
  2. Drop the .mbnt bundle into the verifier.
  3. Drop the original file.
  4. Click Verify.

The verifier auto-detects sealed mode (via canonical.subject.kind == "file_anchor"), reads the master salt from the manifest, recomputes HMAC + per-leaf HKDF + Merkle root, compares to the bundle's stored values, then fetches the on-chain transaction and confirms the canonical-doc hash matches.

Outcome: a single card showing one of the verifier's real labels — "Fully verified" (bundle + original file both check out and the chain matches), "Anchored proof verified" (bundle alone, no original file supplied — a qualified success, not an error), "File does not match" / "Proof does not match its anchor" (a recompute failed), or "Chain lookup failed".

Manual / scripted verification (for skeptical recipients)

When the recipient prefers to verify with off-the-shelf tools rather than trust a single web page, walk them through this:

# 1. Open the bundle
unzip -d bundle/ <bundle>.mbnt

# 2. Read the master salt (base64url, padding stripped) and decode to
#    32 bytes. salt_b64 uses the URL-safe alphabet (- and _) with no
#    "=" padding, so translate to standard base64 and re-pad before
#    `base64 -d` (plain `base64 -d` rejects it as "invalid input").
SALT_B64=$(jq -r .salt_b64 bundle/manifest.json)
STD=$(printf '%s' "$SALT_B64" | tr '_-' '/+')
while [ $(( ${#STD} % 4 )) -ne 0 ]; do STD="${STD}="; done
printf '%s' "$STD" | base64 -d > /tmp/master_salt
wc -c /tmp/master_salt   # must be 32

# 3. Recompute byte_exact_commitment and compare. The commitment is
#    HMAC-SHA256 keyed on the RAW 32 salt bytes (not the ASCII of the
#    hex string), so pass the hex via `hexkey:` — `key:` would HMAC
#    with the wrong key and never match a genuine proof.
openssl dgst -sha256 -mac HMAC -macopt hexkey:$(xxd -p -c 99 /tmp/master_salt) \
    < /path/to/original_file
# Compare the hex after "= " to canonical.json's
# subject.proofs.byte_exact.commitment.

# 4. (Optional) For chunk_merkle: derive per-leaf salts via HKDF,
#    HMAC each chunk's canonical bytes with its derived salt, build
#    Merkle root, compare to canonical.json's
#    subject.proofs.chunk_merkle.root.
#    (Per-leaf salt = HKDF-SHA256 of the master salt over the chunk
#    index; leaves are HMAC-SHA256 of canonical chunk bytes; the root
#    is the standard pairwise hash up the tree.)

# 5. Hash the STORED canonical.json bytes directly and compare the
#    sha256[:40] to the on-chain document_hash. Do NOT re-canonicalize
#    (no JCS / no jq re-serialization): the bundle stores the exact bytes
#    that were hashed on-chain (web/storage.py and `mb bundle` both write
#    the minimal canonical form), so hashing the file verbatim is both
#    simpler and correct. A re-serializer such as jq does NOT NFC-normalize
#    and would false-"tamper" a valid bundle whose canonical.json contains
#    non-ASCII / non-NFC content.
sha256sum bundle/canonical.json | cut -c1-40
# Compare to:
#   - manifest.json's `doc_hash_expected` field
#   - the on-chain MBNT OP_RETURN's document_hash

# 6. Fetch the tx from any BSV block explorer and confirm the
#    OP_RETURN payload starts with bytes "MBNT" and contains the
#    same document_hash.

If steps 3, 5, and 6 all match, the file existed in this exact form at the time of the on-chain transaction. The chain timestamp is established by the block timestamp; the rest is a deterministic computation anyone can re-run.


Operational hygiene before disclosure

These are the things that go wrong in practice. None are cryptographic; all are procedural.


Pitfalls

"I lost the bundle but I have the file."

The proof is unverifiable. There is no recovery path. The chain entry exists but no one — including you — can prove which file it corresponds to without the master salt.

Mitigation: bundle backup discipline at the moment of notarization. Email a copy to a personal address you control; print and lock it; commit a copy to a private repo. Anywhere you'd back up a passphrase.

"I lost the file but I have the bundle."

Same — unverifiable. The bundle commits to the file's HMAC; without the file you cannot reconstruct the input to the HMAC.

Mitigation: same. The file and the bundle together are what proves possession. Lose either, the seal is broken.

"I want to selectively disclose more chunks later."

Each selective disclosure reveals the per-leaf salt for that chunk index. Doing it incrementally is fine — disclosing chunk 3 doesn't unseal chunks 4, 5, 6. But the chunks you already disclosed stay disclosed; you cannot retract.

Mitigation: think before each reveal about what stays sealed afterward. If you might want to disclose chunk 7 later, that's safe even if you've disclosed chunk 3. If you might want to unsay disclosure of chunk 3, you can't.

"I want to anchor the same file twice with different salts."

This works fine — sealed mode does not deduplicate by file hash, by design (per spec §7.2). Each submission with mode=sealed produces an independent proof with its own salt and proof id. The chain shows two entries, but neither reveals that they correspond to the same underlying file.

Mitigation: if you want a second anchor for the same file (different recipient, different timeframe), just submit again. Two sealed proofs for the same file cannot be correlated by anyone who doesn't hold both bundles.

"An adversary is watching the chain."

They learn: timestamps, sizes (via the chain transaction's bytes), the fact that some sealed proofs are being anchored. They do NOT learn which files. If your operational threat model includes correlation of broadcast timing, IP addresses, and metadata patterns, sealed mode does not solve that — use Tor / a clean VPN / a dedicated machine for the submission.


Quick reference

If you want to...Do this
Make the proof publicMode 1: publish bundle + file together.
Reveal one piece without unsealing the restMode 2: publish chunk + per-leaf salt + Merkle path.
Hand over to one party (legal)Mode 3: deliver bundle + file under your existing confidentiality agreement.
Verify your own seal before disclosingDrop bundle + file into /verify; expect "Fully verified".
Re-verify years from now without usSave a copy of the verifier HTML file plus the bundle plus the original file. The chain step needs any working BSV block explorer.

When to NOT use sealed mode

If your goal is simply to publish a timestamp — code releases, public attestations, customer-facing receipts (the documents themselves), any artifact whose existence is already public — use the default public mode at https://proof.satsignal.cloud/. Sealed mode adds operational discipline (bundle is bearer, no recovery on loss) that's only worth taking on when the existence of the proof is itself sensitive.

The two modes are not "more secure vs. less secure." They're for different situations:

Most use cases want the first. A small but valuable set need the second.

Questions about this specification? Email hello@satsignal.cloud.