Security posture, in plain language.
A short, specific account of how Satsignal handles the security-sensitive parts of creating a proof: hashing, third-party code, transport, what’s stored where, and how to report a problem. Where a claim is verifiable, we link to the artifact.
Six controls across proof creation, delivery, and verification.
Two of them — browser-side hashing and the no-file-bytes bundle format — hold even if Satsignal goes away; the other four harden the hosted service while it’s running.
Hashing happens in your browser
On the file-proof path — standard and sealed alike —
your file never leaves the page. The browser reads it locally with
the File API, computes SHA-256 (and the format-specific canonical
fingerprints) via the built-in crypto.subtle, and posts
only the resulting short hex strings. Two narrow, disclosed
exceptions where content does reach the server — incoming
webhook bodies and plaintext provenance manifests — are noted
under reporting and in the
DPA.
File-proof bundles never contain file bytes
Your file-proof .mbnt bundle is a small zip with a
manifest and a canonical JSON document — never the original
file’s bytes. This is a property of the format, not a server
policy: no code path writes file bytes into a bundle. (A
plaintext provenance manifest is a different, structured
artifact that does travel inside its bundle — see the
DPA; sealed provenance keeps it
client-side.)
Third-party code is vendored and pinned
Every JavaScript dependency Satsignal loads is hosted at
/static/ on our origin and pinned with a Subresource
Integrity (SRI) hash. There is no runtime CDN load. Tampering at
the static path or in transit produces a browser-side integrity
failure, not a silent compromise.
HTTPS only, with HSTS
Every vhost is served over TLS from a standard certificate
authority and sets Strict-Transport-Security:
max-age=31536000; includeSubDomains. After a first visit,
the browser refuses cleartext for the apex and every
subdomain.
Content Security Policy
Every page sets a CSP that restricts script-src to
our origin (with the SRI-pinned vendored libraries),
frame-ancestors to 'none' so other sites
can’t iframe our pages (this blocks iframe embedding, not the
API-level embedding in the embedding
rider), and connect-src to the public BSV explorers
we look transactions up against (WhatsOnChain, with a Bitails
fallback).
Rate limiting on writeable endpoints
Anchor creation endpoints — POST /notarize
(the file-proof form on proof.* / sealed.*) and
POST /api/v1/anchors (the bearer-auth programmatic
API at app.satsignal.cloud) — are rate-limited per source
IP and per workspace respectively, to prevent automated abuse and
keep operator wallet spend bounded. Magic-link login is also
rate-limited per source IP and per recipient address, and —
when configured — gated behind a Cloudflare Turnstile
bot-protection challenge (which receives the source IP and the
usual browser request signals at sign-in; see the
subprocessor list).
The offline cryptographic verifier, proof-page fetches, and
bundle downloads are not rate-limited — we want anyone to
be able to verify a proof they already hold, any time. The one
verification-adjacent endpoint that is capped is the hash
discovery oracle /lookup_hash (it rebinds a hash to
its on-chain txid when you don’t hold the bundle): 120
requests/hour per source IP, higher with a bearer token.
Exactly what JavaScript loads, and from where.
Each library is vendored verbatim from its canonical npm-published
source, served from the same origin as the page that uses it, and
loaded with a integrity="sha384-…" attribute. The
browser will refuse to execute a file whose hash doesn’t match.
jspdf.umd.min.js(jsPDF 4.2.1 — MIT)- Renders the printable PDF proof browser-side. No server-side PDF code path.
jszip.min.js(JSZip 3.10.1 — MIT)- Reads
.mbntbundles in the verifier and packages the proof-package zip on the proof page. pdf.min.mjs+pdf.worker.min.mjs(PDF.js 5.7.284 — Apache-2.0)- Extracts text from PDF inputs in the browser to compute the
content-canonical fingerprint. Runs entirely in a Web Worker;
isEvalSupported:falseis set explicitly ongetDocument()so untrusted PDFs cannot reach the eval-based renderer fallback. The file never leaves the page. qrcode.js(qrcode-generator 1.4.4 — MIT)- Encodes the proof URL as a QR code on page 1 of the printable PDF. Vendored unminified on purpose: the jsdelivr-minified build is dynamically generated and explicitly NOT SRI-stable. The original npm-published source is.
Licenses. jsPDF, JSZip, and qrcode-generator are MIT;
PDF.js is Apache 2.0. JSZip is offered under MIT or GPLv3 —
we use it under the MIT license. The page fonts, Inter and JetBrains
Mono, are under the SIL Open Font License 1.1. Each library and
font retains its original copyright and license notice in the
vendored file (the font license texts ship alongside the fonts under
/fonts/). All are permissive licenses — none impose
copyleft obligations on the rest of the site.
The verifier at /verify is itself a single static HTML file with its verification code embedded in the page — save it, audit it, and a local copy still runs its hash and fingerprint checks offline against any saved bundle. Two loads stay on the network: the on-chain lookup (WhatsOnChain, with a Bitails fallback), and PDF.js, fetched on demand only for PDF text re-extraction.
Two short lists.
What we keep
- Your bundle (manifest + canonical doc, never file bytes), at
/bundle/<id>.mbnt— retained until you delete it — from your dashboard or on request; no automatic expiry. Your downloaded.mbntstays verifiable forever regardless. - Sealed-mode bundles submitted with the mirror option: same shape, plus the master salt; kept until you delete the proof, unless the anchor set an explicit retention window — then auto-deleted when that window ends (the date is printed on the proof). Blind sealed submissions are never stored on our server.
- One HTTP access-log line per request — timestamp, source IP, path, status — rotated automatically
- The bundle ID — so the URL resolves while the server-side bundle exists (until you delete it)
What we don’t keep
- Your file’s bytes — in standard mode, they never reach the server
- Filename or memo on the public chain — never. If you provide either, it’s kept in our private server-side record — not in the
.mbntbundle you download and share — and never gets written to chain - No application lookup index keyed by IP or filename. Standard-mode proofs are resolvable by their file’s SHA-256 through the public
/lookup_hashendpoint (documented behavior — sealed proofs are never resolvable this way); bundles are otherwise addressable only by their opaque ID
One key signs the anchors. Here is what it can and can’t do.
Satsignal anchors are signed by a single BSV account key (an
xprv) supplied to the notary process through an
environment variable — held in a mode-0600 env file on the
production host, never committed to the repository, never written
into a bundle, and never transmitted to a customer. There is no HSM
and no multi-party signing today; multi-party / hardware-backed
signing is on the roadmap.
What a compromise of that key could do: sign new
anchors going forward — an attacker holding the key could mint
fresh proofs that look operator-issued. What it cannot do:
alter, delete, backdate, or invalidate any anchor that already
exists. Those are fixed in confirmed Bitcoin SV transactions the key
has no power over; every .mbnt bundle already
downloaded keeps verifying offline against the chain regardless. The
blast radius of a key compromise is forward-only, and the response
is to rotate the signing key and re-anchor — existing
customer proofs are unaffected.
What this means for verifying a proof: trust the binding, not the
brand. A Satsignal anchor carries an issuer_id tag so
anchors we minted can be discovered as a class on-chain — but that
tag is a discoverability handle, not an authenticity guarantee.
Because authenticity is “this key signed it,” anyone holding
the signing key could mint an anchor bearing the same tag. The thing that
actually proves your document is the doc_hash the anchor
commits to: re-compute the hash of the file you hold and confirm it
matches the anchor (the in-browser verifier does this offline). Verify
the doc_hash binding rather than relying on the
“issued by Satsignal” tag.
Key rotation and incident response. We keep a written
rotation/incident runbook for the signing key. If it is, or might be,
compromised, the response is: stop signing immediately, rotate to a new
off-host key, resume anchoring under the new key, and publish a rotation
notice here so relying parties can watch for it. Already-confirmed proofs
are unaffected — their on-chain commitments do not depend on the key
after confirmation, which is exactly why re-verifying the
doc_hash (above) is the durable check.
On the roadmap. Shrinking the blast radius of that single key is a priority: multi-party (threshold) signing so no single key can mint an anchor alone, and hardware-backed key custody (an HSM or signing device) so the key material never sits in process memory. These are tracked, not shipped — we will keep saying so plainly here until they are.
Tell us before you tell anyone else.
Email hello@satsignal.cloud with a description of the issue, steps to reproduce, and any proof-of-concept artifacts. We aim to acknowledge within a few business days. Use a non-Satsignal address (don’t put a sensitive PoC in our hash-lookup endpoint).
We follow ordinary responsible-disclosure norms: we’d rather fix and ship before public disclosure, and we’ll credit you in the fix commit and a brief note here unless you prefer otherwise. No bounty program at this size.
To report abusive or prohibited content behind a proof page (as opposed to a security vulnerability) — for example an anchor whose very existence furthers harm — email abuse@satsignal.cloud with the proof URL or proof ID. We cannot inspect anchored files (by design we never receive them), so enforcement relies on such reports; we can suspend new anchoring, terminate the responsible account, and de-list a proof page from our own surfaces, but the permanent on-chain anchor itself cannot be un-published. See the acceptable-use policy.
Triage notes:
- Theoretical attacks on the Bitcoin SV chain itself (chain reorgs, miner collusion, the chain ceasing to operate — those are not Satsignal’s posture to defend).
- Issues that require a compromised endpoint (your laptop, your browser extension) — we can’t protect what we don’t see.
- Operator wallet, signing-key, and server-access issues are handled as infrastructure-security reports rather than client-data privacy reports. Customer file bytes never reach our servers on the file-anchoring paths in either mode, but operator key compromise could affect service integrity and spend controls — mail those to the same address and we’ll triage as ordinary security-sensitive issues. Two documented exceptions to “never reaches us”: incoming webhook event bodies necessarily transit the server (read to verify the source’s signature and compute the SHA-256; the body is not stored), and plaintext provenance manifests are hashed server-side and travel inside the bundle (sealed provenance avoids that).
- Generic findings from automated scanners with no concrete impact — we welcome them but they’re lowest priority.
Acknowledgments. We credit researchers who report valid issues here (and in the fix commit) unless they prefer to stay anonymous. No reports to acknowledge yet — be the first.