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 properties that hold whether or not Satsignal is online.
Hashing happens in your browser
Standard mode never sends your file. The page reads the file
locally with the File API, computes SHA-256 (and the
format-specific canonical fingerprints) via the browser’s
built-in crypto.subtle, and posts only the resulting
short hex strings.
Bundles never contain file bytes
Your .mbnt bundle is a small zip with a manifest and
a canonical JSON document — no original-file bytes. This is a
property of the format, not a server policy: there is no code path
that writes file bytes into a bundle.
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
Caddy fronts every vhost with TLS from Let’s Encrypt 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 disallow embedding, and
connect-src to the public BSV explorers we look
transactions up against.
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 API key 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.
Verification, receipt fetches, and bundle downloads are not
rate-limited; we want anyone to be able to verify any receipt,
any time.
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)- Renders the printable PDF receipt browser-side. No server-side PDF code path.
jszip.min.js(JSZip 3.10.1)- Reads
.mbntbundles in the verifier and packages the proof-package zip on the receipt page. pdf.min.mjs+pdf.worker.min.mjs(PDF.js 5.7.284)- 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)- Encodes the receipt 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.
The verifier at /verify is itself a single static HTML file. You can save it, audit it, and run it offline against any saved bundle.
Two short lists.
What we keep
- Your bundle (manifest + canonical doc, never file bytes), at
/bundle/<id>.mbnt - Sealed-mode bundles submitted with the mirror option: same shape, plus the master salt; auto-deleted after the retention TTL printed on the receipt. 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 itself, so the URL keeps resolving
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 lives in the receipt bundle on our server (and in your downloaded copy), but never gets written to chain
- Per-IP rate-limit counters across service restarts — in-memory only
- Any index keyed by IP, hash, or filename — bundles are addressable only by their opaque ID
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.
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 files never reach our servers 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.
- Generic findings from automated scanners with no concrete impact — we welcome them but they’re lowest priority.