Security implications
An OrangeCheck attestation is a public, signed artifact linking a Bitcoin address to a set of self-asserted handles. That is the whole point — but it has real consequences. Read this page before you publish your first attestation.
TL;DR
- Publishing an attestation permanently links your Bitcoin address to every handle you asserted. You cannot un-link.
- The bonded UTXO's sats and age are advertised publicly. This is wealth advertisement on a transparent ledger.
- OrangeCheck proves control + stake. It does not prove personhood, does not prove handle ownership, and does not resist chain analysis.
- For high-stakes contexts (payroll, payments, one-vote-per-human) you need an additional layer.
What the attestation reveals
| Reveal | Source | Risk |
|---|---|---|
| The address | Signed message | Anyone can now associate your handles with that address' on-chain history: balances, past counterparties, change addresses (via heuristic clustering), spending patterns. |
sats_bonded | Chain state | Public wealth advertisement. Non-trivial amounts (> ~0.1 BTC) can make you a target for social engineering, phishing, or physical coercion ("wrench attack"). |
days_unspent | Chain state | Advertises that the UTXO has been idle for N days. Signals cold-storage-style holdings. |
Every handle in identities: | Signed message | Cross-platform correlation. github:alice + nostr:npub1… + twitter:@alice creates a strong tracking vector that persists even if you later rotate one handle. |
None of these are bugs — they are what the protocol is designed to prove. But you are the one choosing to publish. Treat publication as "this address is now forever public" and decide accordingly.
Threats to the signer
Chain analysis & pseudonymity loss
Bitcoin is transparent. Once an address is bound to a handle, chain-analysis firms and anyone with a blockchain explorer can:
- See the full transaction history of that address.
- Cluster co-spending inputs to link adjacent addresses in the same wallet.
- Estimate total wallet size beyond just the bonded UTXO.
- Correlate with exchange deposit/withdrawal patterns.
Mitigation: Use a dedicated attestation address funded from a CoinJoin output or a fresh input with no correlation to your main stack. Treat this address as burnt-for-publicity from day one.
Wealth advertisement ("wrench attack")
sats_bonded is a public number. Combined with a real-name handle (github:yourrealname, twitter:@yourrealname), it lets attackers enumerate wealthy targets by handle. The standard mitigation is never bind a real-name identity to an address with material value.
Mitigation:
- Keep attestation addresses modest. The protocol is honest-signal-floor, not a leaderboard.
- Prefer pseudonymous handles (
nostr:npub1…, pseudonymousgithub:handle) over legal-name-linked handles. - Do not bind geographic identifiers (
dns:yourhome.com) unless you have accepted the physical-safety tradeoff.
Cross-platform correlation
Each attestation binds multiple handles to the same address. Binding nostr:npub1… + github:alice + twitter:@alice is a stronger correlation than any of them alone — a third party now knows these are the same person.
Mitigation: Publish separate attestations for separate personas (different addresses, different handle sets). Do not bind your "work identity" and your "anonymous identity" in the same signed message.
Loss of fungibility on the bonded UTXO
Once a UTXO is publicly labeled via an attestation, chain-analysis tooling can tag it. Exchanges with strict compliance may treat future spends from that UTXO differently (enhanced screening, delayed crediting). This is unlikely for small bonds, but non-zero.
Mitigation: After an attestation's useful lifetime, consolidate the UTXO into a CoinJoin output before using it for anything sensitive.
Key loss / theft
The attestation proves control at signing time. If your keys are later lost or stolen:
- Lost keys: you cannot issue new attestations from that address. The old one remains valid until the UTXO is spent by someone else (which you can't do without the keys).
- Stolen keys: the attacker can sign new attestations binding new handles to your address. There is no revocation; the only "revoke" is to spend the UTXO (which also requires the keys). A public revocation event on Nostr is the soft mitigation.
Mitigation: Hardware wallet for any address you intend to attest from. Treat an attestation address like a hot wallet for signing and a cold wallet for storage — the same threat model applies.
Threats to the verifier
Wrong-active-account signatures
The #1 cause of signature-invalid errors in production: the user's wallet is switched to a different account than the one they typed into the form. The signature is valid — for the wrong address.
Mitigation: Before signing, the client should call the wallet's requestAccounts() (UniSat / Leather) or getAddresses() (Xverse) and verify it matches the address being challenged. @orangecheck/wallet-adapter does this automatically.
Stale chain state
A valid signed attestation proves control at signing time. By the time a verifier checks it, the UTXO may have been spent.
Mitigation: Always re-derive sats_bonded and days_unspent from live chain state, never trust the values inside the signed message. GET /api/check and the SDK's verify() do this by default.
Relay censorship / partition
If you discover attestations via a single Nostr relay, that relay can hide or selectively drop events. Partitioned relays can serve different views of the world.
Mitigation: Query multiple relays (≥ 3) and union the results. The reference @orangecheck/sdk defaults to wss://relay.damus.io, wss://relay.primal.net, and wss://nos.lol.
BIP-322 implementation bugs
Incorrect message hashing or signature verification can silently pass invalid signatures.
Mitigation: Use a well-tested BIP-322 library. The reference TypeScript SDK (@orangecheck/sdk) wraps bitcoinjs-lib; the Python SDK uses the bip322 PyPI package, which bridges the audited Rust bitcoin + secp256k1 crates. Both SDKs pass the protocol's normative conformance vectors — if you implement your own verifier, you MUST pass these too.
Sybil at the economic floor
An attacker with N × min_sats can produce N valid attestations from N addresses. OrangeCheck raises the cost of sybils; it does not eliminate them.
Mitigation: Choose min_sats high enough that N × min_sats exceeds the attacker's expected value from gaming your system. For anonymous forum posts, 10k sats is plenty. For airdrops worth $1k/slot, demand 100k+ sats and 90+ days.
Identity claim spoofing
The identities: list is self-asserted. A signer can claim to be github:alice when they are not.
Mitigation: Verify handle ownership out-of-band. The standard method for each protocol is documented in Identity bindings — GitHub gist containing the attestation_id, DNS TXT record, signed Nostr event, etc. The hosted /api/check endpoint does this automatically for nostr, github, and dns.
Protocol-level mitigations (already enforced)
The following are baked into the spec and the reference implementations — you get them for free:
| Risk | Mitigation |
|---|---|
| Signature replay across sites | Canonical message binds to audience (the expected RP). Signing for ochk.io does not work on example.com. |
| Signature replay across uses | purpose field (attestation, login, vote) prevents a login signature from being used as an attestation. |
| Challenge replay | One-time nonce in the signed-challenge flow. |
| Message-format ambiguity | Canonical message is byte-exact; any whitespace / ordering drift = sig_invalid. Cross-SDK conformance vectors lock this. |
| Cross-impl drift | TS + Python SDKs ship identical normative test vectors (tv01–tv23). CI fails if either drifts. |
| Signature malleability | BIP-322 simple signatures are canonical-form; low-S enforced. |
Sign-in-with-Bitcoin specifics
If you are running the challenge-response auth flow from /docs/guides/sign-in-with-bitcoin:
- Session cookies must be
httpOnly,Secure, andSameSite=Lax(orStrict). The reference implementation does this. - Nonce TTL should be short — 5 minutes is reasonable. Longer TTLs widen the window for phishing-then-replay.
audienceandpurposemust be set on every challenge and checked on verify. Do not accept a challenge issued for a different host or purpose.- Rate-limit
/challengeper IP to prevent enumeration-style attacks. - CSRF: state-changing endpoints should require the session cookie plus a
SameSite=Laxguard. The reference handlers do this. - Session revocation: delete the session row in your DB. The cookie is opaque and unsigned on the client side.
What OrangeCheck does NOT protect against
| Not protected | Why |
|---|---|
| Proof of personhood | Address control is not humanness. One person can hold many addresses; one address can be shared. |
| Proof of uniqueness | A wealthy attacker can split a bond into many smaller bonds. Raise min_sats to raise their cost. |
| Handle ownership | Self-asserted. Verify out-of-band. |
| Chain analysis | Bitcoin is transparent by design; this protocol publishes on top of that. |
| Physical coercion | If someone has your keys, they can sign anything you can. |
| Compromised wallet software | A malicious wallet could sign something other than what you see. Use wallets you trust. |
| Zero-knowledge claims | OrangeCheck attestations are public by design. ZK variants are future research, not current protocol. |
Checklists
If you are a signer (publishing attestations)
- Do you understand that this address will be publicly linked to the handles you bind — forever?
- Is the address funded from a source that does not leak your other holdings?
- Is
sats_bondedat a level you are comfortable advertising publicly? - Have you avoided binding real-name handles to material balances?
- Is the signing key in a hardware wallet (or otherwise cold)?
- Have you kept a backup of the seed phrase?
If you are an integrator (calling /api/check or the SDK)
- Are you choosing
min_satslarge enough thatN × min_satsexceeds the attacker's payoff at scale? - For high-stakes contexts, are you also requiring
min_days ≥ 30(prevents flash-bonds)? - Are you verifying handle ownership out-of-band if your gate depends on it?
- For login-style flows, are you setting
audience+purpose+noncecorrectly? - Are you cache-busting
/api/checkresults when making high-stakes decisions? (Default: 60s cache.)
If you are self-hosting the verifier
- Is your Esplora-compatible node trusted, or are you falling back to public explorers with multiple sources?
- Are you querying multiple Nostr relays per verification?
- Are BIP-322 verification crates up-to-date? (CVEs in
bitcoin/secp256k1ripple into everything.) - Are session cookies
httpOnly + Secure + SameSite? - Is rate-limiting in place on every issuer + verifier endpoint?
Reporting a vulnerability
Protocol-level security issues: open an advisory at github.com/orangecheck/oc-protocol/security/advisories. Reference-implementation bugs in the SDKs / gate / CLI: github.com/orangecheck/oc-packages/security/advisories. Do not open public issues for exploitable bugs.
Further
- Identity bindings — why handle claims are not handle proofs.
- Verification — the exact checks a verifier performs.
- Sign-in-with-Bitcoin — challenge-response auth with session hygiene notes.