6.7 KiB
Hephaestus Wallet-Auth Protocol
Version: 0.1 (MVP) Signing scheme: BIP-137-style "Bitcoin Signed Message" on Bitcoin Cash
The point of this document is that any BCH wallet — Electron Cash, Cashonize, Zapit, or anything else that implements "Sign Message" — can produce a signature Hephaestus will accept. There is no Hephaestus-specific signing format.
Flow
┌─────────┐ ┌──────────────┐ ┌──────────┐
│ Browser │ │ Auth-proxy │ │ Forgejo │
└────┬────┘ └──────┬───────┘ └─────┬────┘
│ │ │
│ 1. GET /login (redirected here by Forgejo /authorize)│
│◀─────────────────────────┘ │
│ │
│ 2. POST /challenge { cashaddr, state, redirect_uri }│
│─────────────────────────▶│ │
│◀── { nonce, message } ───│ │
│ │ │
│ 3. Sign `message` with wallet (client-side) │
│ │ │
│ 4. POST /verify { nonce, signature } │
│─────────────────────────▶│ │
│◀── { redirect: <forgejo-callback>?code=…&state=… } ──│
│ │
│ 5. GET <forgejo-callback>?code=…&state=… │
│──────────────────────────────────────────────────────▶│
│ │ 6. POST /token { code } │
│ │◀───────────────────────────│
│ │─── { id_token } ──────────▶│
│ │ │
│ 7. Session cookie set, redirect into app │
│◀──────────────────────────────────────────────────────│
Challenge message
Byte-for-byte the same on both sides. Any deviation invalidates the signature.
{DOMAIN} wants you to sign in with your Bitcoin Cash account:
{CASHADDR}
By signing, you prove you control this address. This request will not trigger
a blockchain transaction or cost any fees.
Domain: {DOMAIN}
Nonce: {NONCE}
Issued At: {ISO8601_TIMESTAMP}
{DOMAIN}— public hostname of the deployment (set viaCHALLENGE_DOMAINenv){CASHADDR}— fullbitcoincash:…address the user is proving control of{NONCE}— 32 hex chars fromcrypto.getRandomValues(new Uint8Array(16)){ISO8601_TIMESTAMP}— server-issuedDate.now().toISOString()
Nonces expire after 5 minutes and are single-use.
Signature format
Follows Bitcoin Signed Message (as implemented by Bitcoin Core / Electron Cash):
- Serialize
varint(len(magic)) || magic || varint(len(msg)) || msgwheremagic = "Bitcoin Signed Message:\n"(24 bytes) digest = SHA256(SHA256(serialized))- Sign the 32-byte digest with recoverable ECDSA over secp256k1
- Build the 65-byte compact signature:
header || r(32) || s(32)whereheader = 27 + recoveryId + 4(the+4marks a compressed pubkey) - Base64-encode the 65 bytes
Verification (on the server, in verify.ts):
- Rebuild the same digest
- Recover the compressed public key from
(compact_sig, recoveryId, digest) - Hash160 the recovered pubkey → derive a p2pkh cashaddr
- Compare against the address the user claimed at
/challengetime - On match: mint an OIDC authorization code bound to the cashaddr
OIDC claims
The id_token Forgejo receives:
{
"iss": "https://auth.hephaestus.silentmode.st",
"aud": "forgejo",
"sub": "bitcoincash:qq…",
"preferred_username": "bitcoincash:qq…",
"iat": 1723200000,
"exp": 1723203600
}
sub— the cashaddr, immutable per user (deterministic from mnemonic)preferred_username— Forgejo uses this to auto-provision the user record on first login- Signed with EdDSA (Ed25519); key is generated on first boot and persisted to
DATA_DIR/keys.json
Threat model
In scope:
- Passive attackers on the network: mitigated by TLS + nonce freshness
- Replay: nonces are single-use with 5-min TTL
- Forgery: requires possession of the mnemonic (24 words) or the private key
- Server compromise leaks challenge nonces but not signing keys — attacker cannot mint valid signatures without the user's wallet
Out of scope (MVP):
- Mnemonic loss = account loss. No recovery. Documented for users.
- Phishing sites collecting signed challenges under our domain string — mitigated by hard-coded
CHALLENGE_DOMAINin the server + user education - Compromised browser (malicious extension reading localStorage) — user's problem
- Post-quantum secp256k1 break — everyone's problem
Phase 2: WalletConnect / mobile signing
WalletConnect is EVM-centric. For BCH the closest equivalents:
- Cashonize (browser wallet) exposes a page-injected
bitcoincashprovider - Zapit (mobile) supports the
pay:URI scheme and could be extended for sign requests - Electron Cash — deep-link URI:
electroncash:?sign=...(proposal, not merged)
Planned surface: after a user hits "Sign with mobile wallet" on the login page, we render a QR code containing hephaestus-sign://{nonce}?domain=…&addr=… and poll /verify for the arriving signature. Mobile app decodes, signs with the user's key, POSTs the signature to our public endpoint with the nonce.
No changes needed to verify.ts for this — signature format is identical.
Phase 2: BCNR name resolution
Every bitcoincash:… address can register a human-readable name via BCNR (Bitcoin Cash Name Records) in the Ariadne stack. On login, if the user's cashaddr has a BCNR name registered, we auto-populate preferred_username with the BCNR name instead of the raw address — matching the BCNR-first policy documented for the broader Silent Mode stack.
Fallback: DNS/traditional identifiers, per project convention.