# 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: ?code=…&state=… } ──│ │ │ │ 5. GET ?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 via `CHALLENGE_DOMAIN` env) - `{CASHADDR}` — full `bitcoincash:…` address the user is proving control of - `{NONCE}` — 32 hex chars from `crypto.getRandomValues(new Uint8Array(16))` - `{ISO8601_TIMESTAMP}` — server-issued `Date.now().toISOString()` Nonces expire after 5 minutes and are single-use. ## Signature format Follows Bitcoin Signed Message (as implemented by Bitcoin Core / Electron Cash): 1. Serialize `varint(len(magic)) || magic || varint(len(msg)) || msg` where `magic = "Bitcoin Signed Message:\n"` (24 bytes) 2. `digest = SHA256(SHA256(serialized))` 3. Sign the 32-byte digest with recoverable ECDSA over secp256k1 4. Build the 65-byte compact signature: `header || r(32) || s(32)` where `header = 27 + recoveryId + 4` (the `+4` marks a compressed pubkey) 5. Base64-encode the 65 bytes Verification (on the server, in `verify.ts`): 1. Rebuild the same digest 2. Recover the compressed public key from `(compact_sig, recoveryId, digest)` 3. Hash160 the recovered pubkey → derive a p2pkh cashaddr 4. Compare against the address the user claimed at `/challenge` time 5. On match: mint an OIDC authorization code bound to the cashaddr ## OIDC claims The `id_token` Forgejo receives: ```json { "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_DOMAIN` in 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 `bitcoincash` provider - **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.