·Security

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.

01Defense in depth

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.

One scoped exception lives off the proof and verification path: the account sign-in step loads Cloudflare’s Turnstile bot-protection widget at runtime from challenges.cloudflare.com. It appears only where you sign in — the app.satsignal.cloud sign-in page and the sign-in prompt on the anchor forms — never on the offline verifier or proof pages, and at that step Cloudflare receives the source IP plus the usual browser request signals (user-agent, headers) — see the subprocessor list.

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.

Metadata fields reject common AI-tool control sequences and control chars at submit time — defense-in-depth, not a wall. Consumers feeding proof fields to an LLM should treat them as untrusted text.

What exactly is rejected

Metadata fields (filename, label, memo, folder label) reject the most common AI-tool control sequences (e.g. <system-reminder> and a handful of related tokens, after Unicode and HTML-entity normalisation), reject C0/C1 control characters and Unicode format characters at submit time, and have server-side length caps. This narrows a known prompt-injection vector against any downstream agent that reads proofs later, but novel control-channel formats and prose-form directives will pass. Consumers that feed proof fields into an LLM should wrap them in <untrusted-field> envelopes or sub-agent isolate. None of this affects the file you’re notarizing.

02Third-party code, line by line

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 .mbnt bundles 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:false is set explicitly on getDocument() 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.

03What’s stored, where, and for how long

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 .mbnt stays 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 .mbnt bundle 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_hash endpoint (documented behavior — sealed proofs are never resolvable this way); bundles are otherwise addressable only by their opaque ID

One thing this storage list omits: backups. Server state is backed up hourly, 3-2-1, encrypted client-side before it leaves the server, to an append-only offsite copy (ciphertext only). When you delete a proof we remove the live copy promptly (within 30 days); it then ages out of those encrypted backups up to about 3 years later as the snapshots expire. Full statement in the DPA.

This page is a plain-language summary. The Privacy notice is the authoritative statement of what we store and for how long, and the DPA is its controller-facing form; where this summary and those differ, treat the Privacy notice and DPA as authoritative.

The Bitcoin SV chain entry is permanent regardless. It contains a 20-byte commitment plus a small structural marker; nothing personally identifying. If you need a server-side bundle removed, ask — we’ll delete it (blind sealed submissions never had one). The on-chain anchor will remain; that’s the property that makes the proof tamper-evident.

04Keys and operator authority

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.

Anchoring authority rests with one operator key at this stage of the product. We state that plainly rather than imply a quorum that doesn’t yet exist; the durable guarantee a proof gives you does not depend on trusting that key after the anchor is confirmed.

05Reporting a vulnerability

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.