docs / security implications

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

RevealSourceRisk
The addressSigned messageAnyone can now associate your handles with that address' on-chain history: balances, past counterparties, change addresses (via heuristic clustering), spending patterns.
sats_bondedChain statePublic wealth advertisement. Non-trivial amounts (> ~0.1 BTC) can make you a target for social engineering, phishing, or physical coercion ("wrench attack").
days_unspentChain stateAdvertises that the UTXO has been idle for N days. Signals cold-storage-style holdings.
Every handle in identities:Signed messageCross-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…, pseudonymous github: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:

RiskMitigation
Signature replay across sitesCanonical message binds to audience (the expected RP). Signing for ochk.io does not work on example.com.
Signature replay across usespurpose field (attestation, login, vote) prevents a login signature from being used as an attestation.
Challenge replayOne-time nonce in the signed-challenge flow.
Message-format ambiguityCanonical message is byte-exact; any whitespace / ordering drift = sig_invalid. Cross-SDK conformance vectors lock this.
Cross-impl driftTS + Python SDKs ship identical normative test vectors (tv01–tv23). CI fails if either drifts.
Signature malleabilityBIP-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, and SameSite=Lax (or Strict). The reference implementation does this.
  • Nonce TTL should be short — 5 minutes is reasonable. Longer TTLs widen the window for phishing-then-replay.
  • audience and purpose must be set on every challenge and checked on verify. Do not accept a challenge issued for a different host or purpose.
  • Rate-limit /challenge per IP to prevent enumeration-style attacks.
  • CSRF: state-changing endpoints should require the session cookie plus a SameSite=Lax guard. 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 protectedWhy
Proof of personhoodAddress control is not humanness. One person can hold many addresses; one address can be shared.
Proof of uniquenessA wealthy attacker can split a bond into many smaller bonds. Raise min_sats to raise their cost.
Handle ownershipSelf-asserted. Verify out-of-band.
Chain analysisBitcoin is transparent by design; this protocol publishes on top of that.
Physical coercionIf someone has your keys, they can sign anything you can.
Compromised wallet softwareA malicious wallet could sign something other than what you see. Use wallets you trust.
Zero-knowledge claimsOrangeCheck 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_bonded at 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_sats large enough that N × min_sats exceeds 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 + nonce correctly?
  • Are you cache-busting /api/check results 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 / secp256k1 ripple 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