Overview.
gitghost is a cryptographic attribution layer for git commits. It lets a developer sign a commit as one of N declared contributors. Verifiers can prove the commit came from the trusted set, but cannot tell which member signed.
The protocol is built on Linkable Spontaneous Anonymous Group (LSAG) signatures over secp256k1 — the same elliptic curve used by Bitcoin and Ethereum. The math is from a 2004 paper by Liu, Wei, and Wong (IACR ePrint 2004/027), with a practical sketch by rot256.
We ship two pieces: a TypeScript CLI (sign / verify / ring management) and this site (public ring registry, browser verifier, documentation). Both halves use the identical LSAG implementation. Anything signed by gitghost commit is verifiable by POST /api/verify, and vice versa.
What gitghost is (and isn't).
It is a way to produce git commits whose authorship is cryptographically provable to belong to a public set, while hiding which specific member signed.
It is not an identity service, a key-management product, an auditing platform, a vulnerability disclosure system, a privacy mixer for blockchains, a proxy for hiding network identity, or a replacement for git commit -S when you want full identity attribution.
If you want anonymity at the network layer (who pushed the commit), use Tor. gitghost only handles cryptographic attribution; it doesn't change TCP/IP packets.
Architecture.
gitghost ships in two halves. A CLI for signing and local verification, and a web app for the public ring registry, browser verifier, and docs. Both share the same LSAG implementation — the math is identical on both sides, so anything signed by gitghost commit verifies via POST /api/verify, and vice versa.
The cryptographic primitives are @noble/curves for secp256k1 and @noble/hashes for SHA-256. Same versions, same algorithms, same byte-level output across CLI and web.
No central server.
The cryptography.
LSAG produces a 1-out-of-N anonymous signature with a deterministic key image per (key, ring) pair. The key image is the linkability hook: the same signer in the same ring always produces the same key image, so reuse can be detected — without revealing identity.
Curve: secp256k1 (same as Bitcoin / Ethereum)
Hash: SHA-256
Hash-to-pt: try-and-increment over compressed-x with even y
Hash-to-sca: H_s(x) = sha256(x) mod n
Wire format: lsag1.<c0>.<s_concat>.<keyImage>
c0: 64 hex (1 scalar)
s_concat: n × 64 hex (n scalars, joined)
keyImage: 66 hex (1 compressed point)I = sk_j · H_p(pk_j ‖ ctx)
sk_j = signer's secret scalar
pk_j = signer's public point
ctx = sha256("gitghost.v1.context|" + ringName)
H_p(x) = hash-to-point of xinput: m, π = [pk_0..pk_{n-1}], j (signer index), sk_j, ctx
output: σ = (c_0, [s_0, …, s_{n-1}], I)
1. I = sk_j · H_p(pk_j ‖ ctx)
2. α ← random scalar
3. L_j = α · G
R_j = α · H_p(pk_j ‖ ctx)
4. c_{j+1} = H_s(m ‖ I ‖ L_j ‖ R_j)
5. for each i ≠ j (around the ring):
s_i ← random scalar
L_i = s_i·G + c_i·pk_i
R_i = s_i·H_p(pk_i ‖ ctx) + c_i·I
c_{i+1} = H_s(m ‖ I ‖ L_i ‖ R_i)
6. s_j = α − c_j · sk_j (mod n)
7. σ = (c_0, [s_0..s_{n-1}], I)input: m, π, ctx, σ
output: bool
c = c_0
for i in 0 … n-1:
L_i = s_i·G + c·pk_i
R_i = s_i·H_p(pk_i ‖ ctx) + c·I
c = H_s(m ‖ I ‖ L_i ‖ R_i)
return c == c_0Reference cryptography uses @noble/curves primitives in plain TypeScript. The implementation matches the LSAG paper directly — no custom curve math, no rolled-our-own crypto.
A commit, end to end.
# inside any git repo gitghost init linux-kernel-core # generates .gitghost/ + secp256k1 identity gitghost ring add-self # add your local pubkey to ring.json gitghost ring add torvalds # fetch github.com/torvalds.keys gitghost ring add gregkh # ...derive deterministic ghost pubkey gitghost ring list # inspect the ring
gitghost commit -m "fix: critical CVE-2026-XXXX" # composes LSAG over N keys # writes Ghost-Ring, Ghost-Ring-Root, Ghost-Key-Image, # Ghost-Signature trailers # runs git commit underneath
gitghost verify <sha> # parses trailers, recomputes ring root, # verifies LSAG, checks key image reuse # exit 0 = valid · exit 1 = invalid / unknown
POST /api/verify
{ "mode": "github", "input": "owner/repo@<sha>" }
# server fetches commit + ring.json from GitHub,
# runs identical verify path, returns JSON resultStandard git commit trailers.
The signature is embedded as RFC 5322-style trailers on the commit message body. Any git tooling that understands trailers will surface them. No fork. No extension.
fix: critical CVE-2026-XXXX Ghost-Ring: linux-kernel-core (4 members) Ghost-Ring-Root: bafkreih7q2zi73p9aplc4eov3iqbjnmhrhuw5kbphr2kk7v2v3iq6q3aaa Ghost-Key-Image: 02a1b2c3d4e5f6... Ghost-Signature: lsag1.<c0>.<s_concat>.<keyImage>
git interpret-trailers --parse <commit>
The public ring registry.
The rings shown at /rings are reproducible from public material. Each member's ghost pubkey is derived deterministically from their public github.com/<user>.keys listing via:
scalar = sha256("gitghost.v1.derive|" + username + "|" + fingerprint)
pk = scalar · G (compressed secp256k1 point)Ring roots are validated against the live cryptography on every boot — if the registry seed drifts from the math, the app fails to start rather than silently shipping wrong data.
Listed members have not endorsed gitghost.
The browser-side verifier.
POST /api/verify runs the same LSAG verify path as the CLI, in the Next.js Node runtime on this site. Two modes:
POST /api/verify
{ "mode": "github", "input": "<commit url or owner/repo@sha>" }
# server fetches the commit message + .gitghost/ring.json
# from the same GitHub commit, then runs verificationPOST /api/verify
{
"mode": "raw",
"message": "<full commit body with Ghost-* trailers>",
"ring": "<ring.json contents>"
}
# server runs verification against supplied data only{
"ok": true,
"trailers": { ringName, ringRoot, keyImage, ringSize, signaturePresent },
"ring": { name, context, members, computedRoot, rootMatches, sourceUrl },
"verification": { signatureValid, keyImage },
"commit": { source, owner?, repo?, sha?, message, htmlUrl? }
}Want browser-only verification?
Ring rewards · Bankr launch.
At Bankr launch, we're committing 5% of token supply to a contributor reward pool. When a ring ships ghost commits, rewards flow to the ring's treasury, not to the signer's personal wallet.
This is deliberate. Auto-transferring tokens to the signer would create an on-chain wallet ↔ ghost commit linkage. Anyone watching the contract could correlate Transfer timestamps with ghost commit timestamps and deanonymize signers. That defeats the entire protocol.
✓ rewards distribute to RING TREASURIES, not signing wallets ✓ ring-treasury → individual claim happens via ring governance or anonymous proof — never auto-bound to signer wallet ✓ no airdrop based on per-commit signing (would invite sybil; rings would farm) ✓ no claim mechanism that requires signing wallet to match the wallet that anchored the commit
Reward mechanism is privacy-first.
What we defend against.
In scope:
- · Passive global observers reading every commit, public key file, and on-chain anchor.
- · Active forgery: an attacker without any ring secret tries to forge a valid signature.
- · Cross-ring deanonymization via key image collision.
- · Ring substitution: swapping a different ring config to falsely claim membership.
- · Replay (mitigated by linkability + anchor logs).
Out of scope:
- · Side channels in the signing host (timing, RAM, malware).
- · Compromise of the signer's secret key — gitghost cannot help if sk leaks.
- · Coercion attacks (someone forces the signer to reveal sk).
- · Network-layer deanonymization of who pushed the commit. Use Tor and a fresh git identity for that.
Cryptographic assumptions:
- · secp256k1 discrete log is hard.
- · SHA-256 is collision-resistant and behaves as a random oracle for hash-to-point and hash-to-scalar.
- · The ring contains ≥ 2 members, and the adversary doesn't control all-but-one.
Known limitations.
Demo-grade, by intent.
- SSH-key derivation is product-grade, not academic-grade. We derive a secp256k1 ghost pubkey from a username + SSH fingerprint. This is reproducible but not cryptographic authentication of the original keyholder. Production rings should use dedicated secp256k1 ghost keys published by each contributor — or, eventually, ed25519 SSH keys + LSAG over edwards25519 (no derivation needed).
- hash-to-point uses try-and-increment. Production should use RFC 9380 hash-to-curve (Simplified SWU) for constant-time and standards compliance.
- The signed message binds to ring root + commit text only, not to the git tree or parent. A future version will bind to tree and parent hashes for full non-malleability.
- No anti-replay binding to commit hash. Mitigated by linkability (same signer → same key image) and anchor logs, but a future version will optionally bind to commit hash.
- Ring root is a deterministic SHA-256, not a real IPFS CID. The format mimics CIDs (bafkrei…) for visual familiarity. Phase 2 swaps in a real CID by pinning the ring config to IPFS.
- On-chain anchoring is live on Base mainnet. The anchor command submits to a deployed GhostRegistry via a sponsored relayer at /api/anchor. Each call records (commit, ringRoot, keyImage) + emits an indexable event. ~$0.0001 per anchor at current Base gas.
- N=2 rings give only 50% anonymity. The CLI warns when ring size is below 5; below 8 you don't have meaningful cover.
What we use, what we don't.
No closed-source dependencies. No telemetry. No analytics beacons. The only outbound network calls are fetch(github.com/<user>.keys), fetch(api.github.com/repos/.../commits/...), and fetch(raw.githubusercontent.com/.../ring.json) — all requests on the user's explicit action.
Reproduce everything.
Every claim on this site is reproducible. Don't take our word for it — recompute it.
# in any git repo gitghost init <ring-name> gitghost ring add-self gitghost ring add <member-1> gitghost ring add <member-2> gitghost ring list # ring root must match what /rings/<slug> shows
# on the verify page, click "load live example" # then submit. our /api/verify returns ok: true. # the same signature verifies via the CLI: gitghost verify e098506b9118fd3abde1a74bd9bb0632ec125c15
# in the ui/ directory npx tsx scripts/seed-rings.mts # fetches latest GitHub keys, re-derives, # rewrites the seed file, validates ring roots