129 lines
6.7 KiB
Markdown
129 lines
6.7 KiB
Markdown
# 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:
|
|
|
|
```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.
|