hephaestus/PROTOCOL.md
2026-09-12 00:07:28 +02:00

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 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:

{
  "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.