w3pk 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +124 -85
- package/dist/index.d.ts +124 -85
- package/dist/index.js +31 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31 -31
- package/dist/index.mjs.map +1 -1
- package/dist/zk/index.js +1 -1
- package/dist/zk/index.js.map +1 -1
- package/dist/zk/index.mjs.map +1 -1
- package/dist/zk/utils.js +3 -3
- package/dist/zk/utils.js.map +1 -1
- package/dist/zk/utils.mjs +3 -3
- package/dist/zk/utils.mjs.map +1 -1
- package/docs/API_REFERENCE.md +87 -18
- package/docs/ARCHITECTURE.md +14 -13
- package/docs/BROWSER_COMPATIBILITY.md +13 -0
- package/docs/INTEGRATION_GUIDELINES.md +36 -20
- package/docs/POST_QUANTUM.md +236 -1468
- package/docs/QUICK_START.md +4 -4
- package/docs/RECOVERY.md +35 -1
- package/docs/SECURITY.md +65 -58
- package/docs/notes/IFRAME_IMPLEMENTATION_PLAN.md +197 -0
- package/docs/notes/JULY_13_ROADMAP.md +29 -0
- package/docs/notes/NOTES.md +333 -0
- package/docs/notes/SIWE_IMPLEMENTATION.md +129 -0
- package/docs/notes/SYNCING_ACROSS_DEVICES.md +127 -0
- package/docs/notes/ethereum-post-quantum-roadmap.md +213 -0
- package/docs/notes/wulong-building-finding.md +109 -0
- package/package.json +3 -2
- package/dist/inspect/node.d.mts +0 -112
- package/dist/inspect/node.d.ts +0 -112
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# SIWE Implementation Review
|
|
2
|
+
|
|
3
|
+
Assessment of the current SIWE (EIP-4361) implementation in [`src/siwe/index.ts`](src/siwe/index.ts), with recommended improvements ordered by importance.
|
|
4
|
+
|
|
5
|
+
**Verdict:** the core is solid and spec-shaped, but there is one real validation bug, several security-hardening gaps, and one DX gap.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Bug: malformed datetimes silently bypass the expiration check
|
|
10
|
+
|
|
11
|
+
**Location:** `validateSiweMessage` — [`src/siwe/index.ts:328-341`](src/siwe/index.ts#L328-L341)
|
|
12
|
+
|
|
13
|
+
`new Date("garbage")` yields an Invalid Date, and any comparison involving `NaN` is `false`. As a result, a message containing `Expiration Time: not-a-date` passes `checkExpiration: true` cleanly:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const expirationDate = new Date(parsed.expirationTime); // Invalid Date
|
|
17
|
+
if (expirationDate < new Date()) { // NaN < ... → false
|
|
18
|
+
errors.push('Message has expired'); // never reached
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Since `validateSiweMessage` is what a server calls to decide whether to grant a session, this is the first thing to fix.
|
|
23
|
+
|
|
24
|
+
**Fix:**
|
|
25
|
+
- Reject the message when `expirationTime`, `notBefore`, or `issuedAt` do not parse to a valid date (`isNaN(date.getTime())`).
|
|
26
|
+
- Ideally also validate RFC 3339 format in `createSiweMessage`, so a nonconforming message can't be signed in the first place.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Security hardening
|
|
31
|
+
|
|
32
|
+
### 2.1 Expiration checking is opt-in and absence-tolerant
|
|
33
|
+
|
|
34
|
+
`checkExpiration` defaults to off, and even when enabled it only checks if the field happens to be present. Attackers get to pick which message they replay, so lenient defaults matter here.
|
|
35
|
+
|
|
36
|
+
**Fix:**
|
|
37
|
+
- Check expiration and not-before **by default**; make skipping the opt-in.
|
|
38
|
+
- Add a way to *require* the presence of an expiration (e.g. `requireExpiration: true`).
|
|
39
|
+
|
|
40
|
+
### 2.2 Lenient parser
|
|
41
|
+
|
|
42
|
+
**Location:** `parseSiweMessage` — [`src/siwe/index.ts:214-242`](src/siwe/index.ts#L214-L242)
|
|
43
|
+
|
|
44
|
+
Current behavior deviates from the EIP-4361 ABNF in ways that create a gap between "what was signed / shown to the user" and "what got validated":
|
|
45
|
+
|
|
46
|
+
- Fields are accepted in **any order** (spec defines a fixed order).
|
|
47
|
+
- **Duplicate fields last-wins** — a message with two `Expiration Time:` lines parses to whichever comes last; another verifier may see it differently.
|
|
48
|
+
- **Unknown lines are silently ignored** — arbitrary appended text is dropped from the parse but covered by the signature.
|
|
49
|
+
- `parseInt("1abc", 10)` yields chain ID `1`; negative chain IDs are accepted.
|
|
50
|
+
|
|
51
|
+
**Fix:** enforce field order, reject duplicate and unrecognized lines, and validate chain ID as a positive integer with no trailing garbage.
|
|
52
|
+
|
|
53
|
+
### 2.3 Nonce modulo bias
|
|
54
|
+
|
|
55
|
+
**Location:** `generateSiweNonce` — [`src/siwe/index.ts:59-67`](src/siwe/index.ts#L59-L67)
|
|
56
|
+
|
|
57
|
+
`x % 62` over a 0–255 byte makes the first 8 characters of the alphabet appear ~25% more often (5/256 vs 4/256). Not practically exploitable at the 11-char default (~65 bits), but rejection sampling is a 3-line fix and removes the finding entirely.
|
|
58
|
+
|
|
59
|
+
**Fix:** rejection sampling — discard bytes ≥ 248 (the largest multiple of 62 below 256) and draw again.
|
|
60
|
+
|
|
61
|
+
### 2.4 No EIP-55 checksum enforcement
|
|
62
|
+
|
|
63
|
+
The spec requires the address to be EIP-55 checksummed. `createSiweMessage` accepts anything and `parseSiweMessage` accepts any-case hex. Verification still works (comparison is lowercased), but strict third-party verifiers will reject messages w3pk produces with a lowercase address.
|
|
64
|
+
|
|
65
|
+
**Fix:** normalize via `ethers.getAddress()` in `createSiweMessage`; validate checksum in `parseSiweMessage`.
|
|
66
|
+
|
|
67
|
+
### 2.5 EOA-only signature verification
|
|
68
|
+
|
|
69
|
+
**Location:** `verifySiweSignature` — [`src/siwe/index.ts:378-422`](src/siwe/index.ts#L378-L422)
|
|
70
|
+
|
|
71
|
+
Verification only does ecrecover. EIP-4361 recommends falling back to ERC-1271 for contract accounts — and since w3pk ships an EIP-7702 module, signatures from delegated accounts are a plausible input.
|
|
72
|
+
|
|
73
|
+
**Fix:** add an optional `provider` parameter enabling ERC-1271 verification (and optionally ERC-6492 for pre-deployment accounts).
|
|
74
|
+
|
|
75
|
+
### 2.6 `verifySiweSignature` doesn't validate the message
|
|
76
|
+
|
|
77
|
+
`valid: true` means only "signature matches the address in the message" — not that the domain, expiry, or nonce are acceptable. Integrators will misread this. [`docs/SECURITY.md:288`](docs/SECURITY.md#L288) already implies the combined check (`ethers.verifyMessage()` + message validation).
|
|
78
|
+
|
|
79
|
+
**Fix:** fold `validateSiweMessage` options into `verifySiweSignature` so one call performs the full server-side check — or document sharply that both calls are required.
|
|
80
|
+
|
|
81
|
+
### 2.7 Missing `scheme` field
|
|
82
|
+
|
|
83
|
+
The current EIP-4361 ABNF allows an optional `scheme://` prefix on the domain line (`https://example.com wants you to sign in...`). The `SiweMessage` type, builder, and parser don't support it.
|
|
84
|
+
|
|
85
|
+
**Fix:** add optional `scheme` to the interface, emit it in `createSiweMessage`, accept it in `parseSiweMessage`, and match it in `validateSiweMessage`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 3. DX: no high-level sign-in helper
|
|
90
|
+
|
|
91
|
+
There is no one-call login flow. Users hand-assemble domain/uri/nonce/issuedAt and then call `signMessage(msg, { signingMethod: 'SIWE' })` — which is just EIP-191 under the hood ([`src/core/sdk.ts:776-781`](src/core/sdk.ts#L776-L781)).
|
|
92
|
+
|
|
93
|
+
**Proposal:** a `w3pk.signInWithEthereum({ statement?, expiresIn?, resources? })` SDK method that:
|
|
94
|
+
|
|
95
|
+
1. Autofills `domain` and `uri` from the current origin.
|
|
96
|
+
2. Generates the nonce and `issuedAt` (plus `expirationTime` from `expiresIn`).
|
|
97
|
+
3. Builds the message, signs it, and returns `{ message, signature, address }`.
|
|
98
|
+
|
|
99
|
+
This turns the common login flow from five manual steps into one call.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 4. Test coverage
|
|
104
|
+
|
|
105
|
+
[`test/siwe.test.ts`](test/siwe.test.ts) is happy-path heavy. Missing negative cases to add alongside the fixes above:
|
|
106
|
+
|
|
107
|
+
- Malformed `expirationTime` / `notBefore` / `issuedAt` (the §1 bug).
|
|
108
|
+
- Duplicate fields and out-of-order fields.
|
|
109
|
+
- Unknown/appended lines.
|
|
110
|
+
- Bad chain IDs (`1abc`, negative, empty).
|
|
111
|
+
- Lowercase vs checksummed addresses.
|
|
112
|
+
- Domain line edge cases (embedded `" wants you to sign in..."` text).
|
|
113
|
+
- Nonce distribution sanity check after the rejection-sampling fix.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Framing notes
|
|
118
|
+
|
|
119
|
+
- **Build vs depend:** an alternative to hardening the parser is depending on the maintained `siwe` npm package. Given w3pk's zero-dependency posture (dynamic `ethers` import only), hardening the in-house implementation is the consistent choice.
|
|
120
|
+
- **No backward compatibility:** per project convention, change `validateSiweMessage` defaults outright (expiration checked by default) rather than adding a compat flag.
|
|
121
|
+
|
|
122
|
+
## Suggested implementation order
|
|
123
|
+
|
|
124
|
+
1. Datetime validation bug (§1) + strict parser (§2.2) + checksum normalization (§2.4)
|
|
125
|
+
2. Safer validation defaults (§2.1) and combined verify+validate (§2.6)
|
|
126
|
+
3. Nonce rejection sampling (§2.3) and `scheme` support (§2.7)
|
|
127
|
+
4. ERC-1271 verification option (§2.5)
|
|
128
|
+
5. `signInWithEthereum()` helper (§3)
|
|
129
|
+
6. Negative-path tests (§4)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Syncing Across Devices
|
|
2
|
+
|
|
3
|
+
How a user who registered on device A (e.g. a laptop) can use the same wallet on device B (e.g. a mobile), what currently happens, and the recommended implementation plan.
|
|
4
|
+
|
|
5
|
+
**Repos involved:**
|
|
6
|
+
|
|
7
|
+
- **genji-passkey** (this app) — [https://github.com/w3hc/genji-passkey](https://github.com/w3hc/genji-passkey) — local path: `/Users/ju/genji-passkey`
|
|
8
|
+
- **w3pk** (the SDK) — [https://github.com/w3hc/w3pk](https://github.com/w3hc/w3pk) — local path: `/Users/ju/w3pk`
|
|
9
|
+
|
|
10
|
+
Findings below are based on w3pk `v0.10.1`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Current behavior (device A → device B)
|
|
15
|
+
|
|
16
|
+
Everything is device-local. There is no server-side session or account.
|
|
17
|
+
|
|
18
|
+
| Data | Where it lives | Syncs across devices? |
|
|
19
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
20
|
+
| Passkey (WebAuthn credential) | OS authenticator | ✅ via iCloud Keychain / Google Password Manager (same ecosystem only) |
|
|
21
|
+
| Credential metadata (username, publicKey, ethereumAddress) | Device-local storage (`/Users/ju/w3pk/src/auth/storage.ts`) | ❌ |
|
|
22
|
+
| Encrypted mnemonic | Browser localStorage (`w3pk_wallet_<address>`) | ❌ |
|
|
23
|
+
| Persistent session | Browser localStorage (`w3pk_session_<address>`) | ❌ |
|
|
24
|
+
| Session duration setting | `localStorage` (`persistentSessionDuration`) | ❌ |
|
|
25
|
+
|
|
26
|
+
**What the user experiences on device B today:**
|
|
27
|
+
|
|
28
|
+
1. The app opens logged out (no persistent session on this device).
|
|
29
|
+
2. Tapping **Login** fails, even when the passkey has synced. After a successful WebAuthn assertion, the SDK looks up the credential metadata in device-local storage and throws when it is missing — see `/Users/ju/w3pk/src/auth/authenticate.ts` (lines 78–86):
|
|
30
|
+
> _"Passkey authenticated but wallet data not found on this device. To sync your account: 1) Select your passkey when prompted, then 2) Provide your backup file."_
|
|
31
|
+
3. In genji-passkey this message does not match `isNoPasskeyError()` in `/Users/ju/genji-passkey/src/context/W3PK.tsx`, so the user sees a raw "Authentication Failed" toast.
|
|
32
|
+
4. The only working self-serve path is **Settings → Restore from Backup** (available logged out), which calls `registerWithBackupFile` — backup file + password + username → new passkey on device B, same wallet address.
|
|
33
|
+
|
|
34
|
+
**Note:** the current "Device Sync" QR feature in `/Users/ju/genji-passkey/src/app/settings/page.tsx` (`handleGenerateQRCode`, ~line 1645) only shares public data (username + addresses). It does not transfer any key material and is not a wallet sync.
|
|
35
|
+
|
|
36
|
+
## 2. What w3pk provides (and its gaps in v0.10.1)
|
|
37
|
+
|
|
38
|
+
The SDK has a dedicated sync API that genji-passkey does not use yet:
|
|
39
|
+
|
|
40
|
+
- **Device A:** `w3pk.exportForSync()` — `/Users/ju/w3pk/src/core/sdk.ts` (~line 2093). Exports a **passkey-encrypted** backup file (no password) plus a QR code when it fits (~2.9 KB limit, `/Users/ju/w3pk/src/sync/backup-sync.ts`, `generateSyncQR`).
|
|
41
|
+
- **Device B:** `w3pk.syncWalletWithPasskey(backupData, password?)` — `/Users/ju/w3pk/src/core/sdk.ts` (~line 1913). Prompts passkey selection, decrypts the backup, stores the wallet, starts a session.
|
|
42
|
+
|
|
43
|
+
**Gaps that prevent this from working on a fresh device:**
|
|
44
|
+
|
|
45
|
+
1. **Passkey-encrypted backups cannot be decrypted on a fresh device.** The decryption key is derived from `credentialId + publicKey` (`deriveEncryptionKeyFromWebAuthn`), and the `publicKey` is only available in device-local storage. On a fresh device, `promptPasskeySelection()` returns an empty `publicKey` (`/Users/ju/w3pk/src/auth/sync-auth.ts`, lines 81–89) and the method throws. The backup file deliberately carries only a 16-char fingerprint of the key (`/Users/ju/w3pk/src/backup/backup-file.ts`, `createPasskeyBackup`) — see the security warning in §5.
|
|
46
|
+
2. **Password-encrypted backups decrypt but are not persisted.** The store-wallet-and-start-session block in `syncWalletWithPasskey` is guarded by `if (credential)` from local storage (`/Users/ju/w3pk/src/core/sdk.ts`, ~line 1984) — `null` on a fresh device, so no wallet is stored and no session starts.
|
|
47
|
+
3. **`importFromSync` requires being logged in first** (~line 2144), which is impossible on a fresh device (see §1, step 2) — circular.
|
|
48
|
+
|
|
49
|
+
## 3. Proposed UX
|
|
50
|
+
|
|
51
|
+
On device B, the register modal (in `/Users/ju/genji-passkey/src/components/Header.tsx`) gains a link:
|
|
52
|
+
|
|
53
|
+
> **"I already registered on another device"**
|
|
54
|
+
|
|
55
|
+
It leads to a sync flow:
|
|
56
|
+
|
|
57
|
+
- **Device A** (logged in): show a QR code containing an encrypted backup of the wallet.
|
|
58
|
+
- **Device B**: scan the QR with the camera, unlock, and complete setup — all on device B.
|
|
59
|
+
|
|
60
|
+
Two implementation options follow. Option 1 ships now with zero SDK changes; Option 2 is the long-term passwordless path.
|
|
61
|
+
|
|
62
|
+
## 4. Option 1 — QR + password (ships now, genji-passkey changes only)
|
|
63
|
+
|
|
64
|
+
Device A displays a QR of a **password-encrypted** backup; device B scans it, the user types the password, and `registerWithBackupFile` creates a new passkey on device B bound to the same wallet.
|
|
65
|
+
|
|
66
|
+
Works with w3pk `v0.10.1` as-is because it reuses the one path that already completes end-to-end on a fresh device.
|
|
67
|
+
|
|
68
|
+
### Changes in genji-passkey (`/Users/ju/genji-passkey`)
|
|
69
|
+
|
|
70
|
+
1. **`src/components/Header.tsx`** — add the "I already registered on another device" link inside the Register dialog, opening the device-B sync screen (or deep-linking to a settings section).
|
|
71
|
+
2. **Device A side (new "Show sync QR" action, e.g. in `src/app/settings/page.tsx`)**:
|
|
72
|
+
- Require a fresh biometric before showing the QR (sign a throwaway message with `requireAuth: true`, or equivalent).
|
|
73
|
+
- Prompt for a password (reuse `src/components/PasswordModal.tsx`), call the existing `createBackup(password)` from `src/context/W3PK.tsx` (which wraps `w3pk.createBackupFile('password', password)`).
|
|
74
|
+
- Render a QR (reuse `qrcode.react`, already a dependency) of a payload like `{ v: 1, kind: 'w3pk-sync', username, backup: <backup file JSON> }`. Embedding the username lets device B prefill it. Size is fine: a password backup is ~600–900 bytes, under the ~2.9 KB single-QR limit.
|
|
75
|
+
3. **Device B side (new sync screen reachable from the register modal)**:
|
|
76
|
+
- Add a QR **scanner** (`qrcode.react` only generates; use the native `BarcodeDetector` API with a library fallback such as `@zxing/browser`), plus a "paste JSON" fallback for devices without camera access.
|
|
77
|
+
- Parse the payload, prefill the username, prompt for the password, call the existing `registerWithBackupFile(backupJson, password, username)` from `src/context/W3PK.tsx`. This creates the passkey on device B and logs the user in.
|
|
78
|
+
4. **Nice-to-have:** in the `login()` catch in `src/context/W3PK.tsx` / `Header.tsx`, detect the SDK's _"Passkey authenticated but wallet data not found"_ message and route the user to the sync screen instead of showing a raw error toast.
|
|
79
|
+
|
|
80
|
+
### Changes in w3pk (`/Users/ju/w3pk`)
|
|
81
|
+
|
|
82
|
+
None required.
|
|
83
|
+
|
|
84
|
+
### Variant 1b — one-time code instead of a password
|
|
85
|
+
|
|
86
|
+
Same architecture, but device A generates a random code, uses it as the backup password, and displays it next to the QR; the user types the code on device B instead of a real password.
|
|
87
|
+
|
|
88
|
+
- Better UX than a full password on a mobile keyboard; a photographed QR alone is not enough to steal the wallet.
|
|
89
|
+
- **Entropy warning:** a captured QR can be brute-forced offline, so a 6-digit code (~20 bits) is too weak even with PBKDF2 at 310k iterations. Use at least ~8 random base32 characters (~40 bits), displayed grouped (e.g. `K7QP-2MWX`).
|
|
90
|
+
- Only genji-passkey changes; the code is just a generated throwaway password.
|
|
91
|
+
|
|
92
|
+
## 5. Option 2 — PRF-based passwordless sync (long-term, w3pk + genji-passkey changes)
|
|
93
|
+
|
|
94
|
+
Goal: device B scans the QR, passes **one biometric prompt**, and is done — no password. Safe because decryption requires possessing the platform-synced passkey, not just the QR.
|
|
95
|
+
|
|
96
|
+
Uses the WebAuthn **PRF extension**: the authenticator derives a secret from `PRF(salt)` that never leaves the authenticator boundary, and **syncs with the passkey** for platform credentials. Support: Chrome 108+, Safari 17+/iOS 17+; not Firefox. Same-ecosystem only (iCloud↔iCloud, Google↔Google) — keep Option 1 as the cross-ecosystem fallback.
|
|
97
|
+
|
|
98
|
+
> **Why not just embed the `publicKey` in the passkey-encrypted backup to fix gap §2.1?** Because the current key is _deterministically derived from `credentialId + publicKey`_ — a file containing the ciphertext plus both inputs would be **self-decrypting**: anyone holding it gets the mnemonic with no hardware or biometric. That is exactly why only a fingerprint is stored today. PRF is the correct fix.
|
|
99
|
+
|
|
100
|
+
### Changes in w3pk (`/Users/ju/w3pk`)
|
|
101
|
+
|
|
102
|
+
1. **`src/wallet/crypto.ts`** — extend key derivation with a PRF path: request the `prf` extension in `navigator.credentials.get()` (and `create()` during registration), derive the AES key as `HKDF(prfOutput, salt)`. (Groundwork partially exists per `docs/SECURITY.md` — `deriveEncryptionKeyFromWebAuthn` / auto-fallback notes.)
|
|
103
|
+
2. **`src/backup/backup-file.ts` + `src/backup/types.ts`** — new `encryptionMethod: 'prf'`: payload `{ ciphertext, prfSalt, credentialId, addressChecksum, username }`. No `publicKey` and no fingerprint needed; the QR alone is useless without the synced passkey + biometric.
|
|
104
|
+
3. **`src/core/sdk.ts` — `exportForSync()`** (~line 2093) — when PRF is available, perform a fresh assertion with PRF evaluation and emit a `'prf'` backup; otherwise fall back to a password backup.
|
|
105
|
+
4. **`src/core/sdk.ts` — `syncWalletWithPasskey()`** (~line 1913) — handle `'prf'` backups: call `navigator.credentials.get()` with `allowCredentials: [credentialId from payload]` and PRF eval using `prfSalt`; derive the key, decrypt, verify `addressChecksum`.
|
|
106
|
+
5. **Fix the fresh-device persistence gap** (~line 1984): when the credential is not in local `CredentialStorage`, still (a) bootstrap credential metadata from the sync payload (username, credentialId; store the wallet under a PRF-derived local key rather than the `publicKey`-based one), (b) store the wallet, (c) start the session and fire `onAuthStateChanged`.
|
|
107
|
+
6. **`src/auth/authenticate.ts`** (lines 78–86) — replace the string-only dead end with a **typed error** (e.g. `SyncRequiredError` carrying the `credentialId`) so apps can catch it and launch their sync flow.
|
|
108
|
+
7. **`src/sync/backup-sync.ts`** — `generateSyncQR` / `parseSyncQR` gain the `'prf'` payload shape; `importFromSync` should no longer require an existing login (uses the same bootstrap as #5).
|
|
109
|
+
|
|
110
|
+
### Changes in genji-passkey (`/Users/ju/genji-passkey`)
|
|
111
|
+
|
|
112
|
+
1. Same UI as Option 1 (register-modal link, device A QR screen, device B scanner).
|
|
113
|
+
2. **Device A:** call `w3pk.exportForSync()` instead of `createBackup(password)`; only fall back to the password QR when PRF is unavailable.
|
|
114
|
+
3. **Device B:** call `w3pk.syncWalletWithPasskey(scannedPayload)` — one biometric prompt, no password. Offer "Use a password instead" for cross-ecosystem or non-PRF browsers (Option 1 path).
|
|
115
|
+
4. Catch the new typed `SyncRequiredError` from `login()` in `src/context/W3PK.tsx` and route to the sync screen.
|
|
116
|
+
|
|
117
|
+
## 6. Security considerations
|
|
118
|
+
|
|
119
|
+
- **A QR code is an exportable artifact.** A fully self-contained passwordless QR would be the mnemonic on a screen: screenshots, screen sharing, or a camera over the shoulder leak it, and client-side TTLs are unenforceable (an attacker decrypts a captured QR at leisure). Something must exist **outside** the QR: the password (Option 1), a one-time code (Variant 1b), or the synced passkey + biometric (Option 2).
|
|
120
|
+
- **Gate QR display on device A behind a fresh biometric** in every variant. This protects against a stolen unlocked laptop — not against QR capture.
|
|
121
|
+
- **"User is logged in on device A" is not sufficient justification for a self-decrypting QR** — the threat is not the user, it's anyone who can photograph or intercept the artifact.
|
|
122
|
+
- Short numeric codes are brute-forceable offline once the QR is captured; see the entropy warning in Variant 1b.
|
|
123
|
+
|
|
124
|
+
## 7. Recommendation
|
|
125
|
+
|
|
126
|
+
1. **Ship Option 1 now** — zero SDK changes, reuses `registerWithBackupFile`, `PasswordModal`, and `qrcode.react`. Upgrade to Variant 1b if password-typing friction matters.
|
|
127
|
+
2. **Pursue Option 2 in w3pk** as the passwordless same-ecosystem path — it also properly fixes the currently dead-ended `syncWalletWithPasskey` / `importFromSync` APIs — and keep the password/code QR as the cross-ecosystem fallback.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Ethereum's post-quantum roadmap: technical details"
|
|
3
|
+
description: How Ethereum plans to replace BLS, ECDSA and KZG with hash-based and lattice primitives, with the parameters, sizes and fork milestones as they stand in September 2026.
|
|
4
|
+
date: 2026-09-13
|
|
5
|
+
locale: en_US
|
|
6
|
+
author: Julien Béranger
|
|
7
|
+
model: Claude Opus 5
|
|
8
|
+
conversation: https://claude.ai/chat/<CONVERSATION_ID>
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The shape of the problem
|
|
12
|
+
|
|
13
|
+
[Ethereum](https://ethereum.org/) depends on four families of public-key cryptography that
|
|
14
|
+
[Shor's algorithm](https://en.wikipedia.org/wiki/Shor%27s_algorithm) breaks:
|
|
15
|
+
[ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) on secp256k1 for
|
|
16
|
+
account signatures, [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature) for validator
|
|
17
|
+
attestations, KZG polynomial commitments for
|
|
18
|
+
[blob data availability](https://eips.ethereum.org/EIPS/eip-4844), and pairing-based SNARKs at the
|
|
19
|
+
application layer. The [Ethereum Foundation](https://ethereum.foundation/)'s
|
|
20
|
+
[Post-Quantum team](https://pq.ethereum.org/) frames the realistic failure mode as stolen funds and
|
|
21
|
+
impersonation rather than rewriting finalized history — past transactions and finalized blocks stay
|
|
22
|
+
valid regardless.
|
|
23
|
+
|
|
24
|
+
The answer is the [lean Ethereum](https://blog.ethereum.org/2025/07/31/lean-ethereum) programme,
|
|
25
|
+
published by [Justin Drake](https://x.com/drakefjustin) on 31 July 2025, which makes the hash function
|
|
26
|
+
the single cryptographic primitive across consensus, data and execution. A dedicated EF Post-Quantum
|
|
27
|
+
team formed in January 2026 under Thomas Coratger. The official target is roughly **2029** for core L1
|
|
28
|
+
infrastructure, with full execution-layer and ecosystem migration extending past that.
|
|
29
|
+
|
|
30
|
+
## Consensus layer: leanXMSS
|
|
31
|
+
|
|
32
|
+
Validator keys move from BLS12-381 to **leanXMSS**, a variant of the
|
|
33
|
+
[eXtended Merkle Signature Scheme](https://en.wikipedia.org/wiki/Hash-based_cryptography) built from
|
|
34
|
+
one-time hash-based signatures organised in [Merkle trees](https://en.wikipedia.org/wiki/Merkle_tree).
|
|
35
|
+
The construction and its security analysis are set out in
|
|
36
|
+
[Hash-Based Multi-Signatures for Post-Quantum Ethereum](https://eprint.iacr.org/2025/055.pdf).
|
|
37
|
+
|
|
38
|
+
| Property | BLS12-381 | leanXMSS |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Public key | 48 bytes | 52-byte Merkle root + 20-byte public parameter |
|
|
41
|
+
| Signature | 96 bytes | 3,112 bytes |
|
|
42
|
+
| Aggregation | native, algebraic | none — requires SNARK |
|
|
43
|
+
| Key lifetime | unbounded | 2^32 slots (~1,632 years at 12s slots) |
|
|
44
|
+
|
|
45
|
+
Registering roughly one million validators expands consensus state by about 52 MiB. Each key derives
|
|
46
|
+
from a single 32-byte master seed.
|
|
47
|
+
|
|
48
|
+
[EIP-8310](https://eips.ethereum.org/EIPS/eip-8310) defines the keystore format for these stateful
|
|
49
|
+
keys. Its recommended production parameter set matches the
|
|
50
|
+
[leanSig](https://github.com/leanEthereum/leanSig) instantiation
|
|
51
|
+
`SIGAbortingTargetSumLifetime32Dim46Base8`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "leanxmss",
|
|
56
|
+
"params": {
|
|
57
|
+
"hash": "<hash identifier>",
|
|
58
|
+
"tree_height": 32,
|
|
59
|
+
"hypertree_layers": 1,
|
|
60
|
+
"encoding": "target_sum",
|
|
61
|
+
"winternitz_w": 8,
|
|
62
|
+
"dimension": 46,
|
|
63
|
+
"target_sum": 200,
|
|
64
|
+
"index_mode": "synchronized",
|
|
65
|
+
"activation_epoch": 0,
|
|
66
|
+
"lifetime_leaves": 4294967296
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The format is deliberately hash-agnostic: the concrete hash lives in the `hash` field, and a reader that
|
|
72
|
+
does not recognise `scheme.name` must refuse to use the keystore.
|
|
73
|
+
|
|
74
|
+
## Aggregation: leanVM
|
|
75
|
+
|
|
76
|
+
Hash-based signatures have no algebraic aggregation, so lean Ethereum replaces group operations with a
|
|
77
|
+
succinct argument — an aggregate becomes a proof that many valid signatures exist, with the verification
|
|
78
|
+
logic compiled into a circuit. **leanVM** is the minimal, [Cairo](https://www.cairo-lang.org/)-inspired
|
|
79
|
+
zkVM that does this, producing one SNARK per slot. The reference implementations live in
|
|
80
|
+
[leanMultisig](https://github.com/leanEthereum/leanMultisig), with an executable
|
|
81
|
+
[Python](https://www.python.org/) specification in [leanSpec](https://github.com/leanEthereum/leanSpec)
|
|
82
|
+
used by around ten client teams.
|
|
83
|
+
|
|
84
|
+
Benchmarks on an M4 Max put leanVM at roughly 1,000 XMSS verifications per second with recursive proofs
|
|
85
|
+
under one second. The stated compression target is 250x, which is what keeps 3 KB signatures from
|
|
86
|
+
blowing up per-slot bandwidth.
|
|
87
|
+
|
|
88
|
+
## The open hash and field question
|
|
89
|
+
|
|
90
|
+
This is the least settled part of the design. Poseidon2, long the default SNARK-friendly choice, is
|
|
91
|
+
under cryptanalytic pressure from recent attacks on algebraic hashes — not broken, but not yet worth
|
|
92
|
+
betting a century of consensus on. The team has floated registering validators under several hash
|
|
93
|
+
functions up front ([Poseidon](https://eprint.iacr.org/2019/458),
|
|
94
|
+
[BLAKE3](https://github.com/BLAKE3-team/BLAKE3),
|
|
95
|
+
[SHA-3](https://en.wikipedia.org/wiki/SHA-3)) purely to preserve agility. The finite-field choice
|
|
96
|
+
between KoalaBear and Goldilocks is also live, the latter offering more proof-size headroom at roughly
|
|
97
|
+
2x slower proving.
|
|
98
|
+
|
|
99
|
+
The tradeoff moved in 2026. Benchmarks on a single M4 Max core put BLAKE3's compression function at
|
|
100
|
+
roughly 82,000 proofs per second, [SHA-256](https://en.wikipedia.org/wiki/SHA-2) at about 42,000 and
|
|
101
|
+
[Keccak](https://en.wikipedia.org/wiki/SHA-3) at around 30,000; across ten cores BLAKE3 exceeded 660,000.
|
|
102
|
+
Hash-friendly SNARKs have reduced the need for SNARK-friendly hashes, so conventional, heavily analysed
|
|
103
|
+
hashes can now enter the design without their former proving penalty.
|
|
104
|
+
|
|
105
|
+
## Execution layer: EIP-8141 frame transactions
|
|
106
|
+
|
|
107
|
+
[EIP-8141](https://eips.ethereum.org/EIPS/eip-8141) introduces a new
|
|
108
|
+
[EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) transaction type (`0x06`) that decomposes a
|
|
109
|
+
transaction into a sequence of **frames** — contract calls that validate the transaction, approve gas
|
|
110
|
+
payment, and execute user operations. A new `APPROVE` opcode moves signature checking and gas payment
|
|
111
|
+
out of rigid protocol rules and into the [EVM](https://ethereum.org/developers/docs/evm/). It builds on
|
|
112
|
+
[EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) and supersedes much of what
|
|
113
|
+
[ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) does with off-chain bundlers.
|
|
114
|
+
|
|
115
|
+
The post-quantum relevance is cryptographic agility: an account defines its own validation logic, so a
|
|
116
|
+
wallet can adopt a post-quantum scheme on its own schedule without a network-wide flag day and without
|
|
117
|
+
a dedicated hard fork per scheme.
|
|
118
|
+
|
|
119
|
+
Mempool safety is specified rather than assumed. Validation work per transaction is bounded by
|
|
120
|
+
`MAX_VERIFY_GAS`, clients normally admit at most one pending frame transaction per sender, and
|
|
121
|
+
validation dependencies are tracked so only affected transactions are revalidated after a new canonical
|
|
122
|
+
block. Failure of a `VERIFY` frame invalidates the whole transaction rather than merely halting a call
|
|
123
|
+
frame. [MATCHA](https://ethresear.ch/t/mempool-account-transaction-capacity-from-historical-activity-matcha/25949)
|
|
124
|
+
proposes relaxing the one-per-sender rule for accounts using
|
|
125
|
+
[EIP-8250](https://eips.ethereum.org/EIPS/eip-8250) keyed nonces.
|
|
126
|
+
|
|
127
|
+
A cluster of proposals extends the frame model:
|
|
128
|
+
[EIP-8272](https://eips.ethereum.org/EIPS/eip-8272) for recent roots,
|
|
129
|
+
[EIP-8209](https://eips.ethereum.org/EIPS/eip-8209) for commit/reveal frames, and
|
|
130
|
+
[ERC-8286](https://eips.ethereum.org/EIPS/eip-8286) binding frame validation to
|
|
131
|
+
[ERC-7579](https://eips.ethereum.org/EIPS/eip-7579) modular accounts.
|
|
132
|
+
|
|
133
|
+
## Precompiles
|
|
134
|
+
|
|
135
|
+
The J\* milestone favours a **vector math precompile** — modular primitives such as
|
|
136
|
+
[EIP-7885](https://eips.ethereum.org/EIPS/eip-7885) for number-theoretic transform operations — over
|
|
137
|
+
whole-scheme verifiers, on the grounds that it avoids locking the protocol to a scheme that later falls
|
|
138
|
+
to cryptanalysis.
|
|
139
|
+
|
|
140
|
+
Whole-scheme drafts exist regardless. [EIP-8051](https://eips.ethereum.org/EIPS/eip-8051) specifies
|
|
141
|
+
`VERIFY_MLDSA` (compliant with [FIPS 204](https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards))
|
|
142
|
+
alongside a cheaper `VERIFY_MLDSA_ETH` that swaps SHAKE256 for a Keccak-based PRNG and stores the
|
|
143
|
+
public-key polynomial `t1` in the NTT domain to skip one transform during verification.
|
|
144
|
+
[EIP-8355](https://eips.ethereum.org/EIPS/eip-8355) extends ML-DSA coverage to NIST security levels III
|
|
145
|
+
and V. Older [Falcon](https://eips.ethereum.org/EIPS/eip-7619) proposals remain on the table; at 1,681
|
|
146
|
+
bytes per signature and 16 gas per non-zero calldata byte, a bare Falcon signature costs about 26,896
|
|
147
|
+
gas just to carry.
|
|
148
|
+
|
|
149
|
+
## Data layer
|
|
150
|
+
|
|
151
|
+
KZG commitments inherit the same pairing weakness. The current mitigation is structural rather than
|
|
152
|
+
cryptographic: the trusted setup holds as long as one participant discarded their secret, and that
|
|
153
|
+
remains true against a quantum adversary working after the fact. The replacements under study are
|
|
154
|
+
[STARK](https://eprint.iacr.org/2018/046)-style hash-based commitments and lattice-based commitments,
|
|
155
|
+
neither yet efficient enough at Ethereum's scale.
|
|
156
|
+
|
|
157
|
+
## Fork milestones
|
|
158
|
+
|
|
159
|
+
The [EF Protocol Architecture](https://strawmap.org/) roadmap places post-quantum work across all three
|
|
160
|
+
layers. These are planning milestones, not commitments — names and ordering may change.
|
|
161
|
+
|
|
162
|
+
| Fork | Milestone | Layer |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| I\* | PQ key registry | Consensus |
|
|
165
|
+
| J\* | PQ signature precompiles | Execution |
|
|
166
|
+
| L\* | PQ attestations, real-time CL proofs, leanVM | Consensus + Data |
|
|
167
|
+
| M\* | PQ signature aggregation, PQ blobs | Execution + Data |
|
|
168
|
+
| Longer term | Full PQ consensus, PQ transactions, PQ sampling | All layers |
|
|
169
|
+
|
|
170
|
+
The nearest real fork is **Hegotá**, targeted for 2027 after
|
|
171
|
+
[Glamsterdam](https://ethereum.org/roadmap/). It is not itself a post-quantum fork; it is the gate that
|
|
172
|
+
decides whether the later PQ forks land on time. The EF's Protocol cluster reviewed 62 candidate EIPs
|
|
173
|
+
with 397 grades from nine client teams and named exactly two must-ship:
|
|
174
|
+
[EIP-7805](https://eips.ethereum.org/EIPS/eip-7805) (FOCIL) and EIP-8141. If either hits technical
|
|
175
|
+
friction, the schedule shifts rather than the proposal being cut. Five further forks follow, averaging
|
|
176
|
+
one every 7.2 months, which forces overlapping rather than sequential execution — a pace Ethereum has
|
|
177
|
+
not previously sustained. A "minimum viable post-quantum" fallback exists for a sudden breakthrough.
|
|
178
|
+
|
|
179
|
+
## Threat model and residual exposure
|
|
180
|
+
|
|
181
|
+
The EF's own ordering of risk surfaces: user accounts first (largest pool of value, public keys exposed
|
|
182
|
+
after the first transaction), then high-value operational keys at exchanges and bridges, then governance
|
|
183
|
+
multisigs, then validator keys — which affect consensus participation rather than directly holding
|
|
184
|
+
assets.
|
|
185
|
+
|
|
186
|
+
"Harvest now, decrypt later" is largely a confidentiality problem, not an ownership one: recording
|
|
187
|
+
transactions today does not enable retroactive theft, since blockchains are integrity systems built on
|
|
188
|
+
signatures. The forward-looking risk is that exposed public keys become derivable once a
|
|
189
|
+
cryptographically relevant machine exists.
|
|
190
|
+
|
|
191
|
+
Dormant funds are the governance question nobody has answered. The EF estimates Ethereum's long-dormant
|
|
192
|
+
exposure at roughly 0.1% of supply, against about 5% for [Bitcoin](https://bitcoin.org/) in early
|
|
193
|
+
address formats. That gap is what makes "do nothing" a more defensible option for Ethereum than it would
|
|
194
|
+
be for Bitcoin.
|
|
195
|
+
|
|
196
|
+
On timing, the EF team is more conservative than the headlines: most engineering roadmaps place
|
|
197
|
+
cryptographic relevance in the early-to-mid 2030s. [Google Quantum AI](https://quantumai.google/)'s
|
|
198
|
+
March 2026 [analysis](https://research.google/blog/safeguarding-cryptocurrency-by-disclosing-quantum-vulnerabilities-responsibly/)
|
|
199
|
+
lowered the estimate for breaking 256-bit elliptic curve cryptography to roughly 1,200 logical qubits,
|
|
200
|
+
and [NIST](https://csrc.nist.gov/projects/post-quantum-cryptography) anticipates deprecating ECDSA by
|
|
201
|
+
2030 and disallowing it by 2035. Verification runs in parallel: the EF's Protocol Snarkification team
|
|
202
|
+
under Alex Hicks is running a $20M, three-year zkEVM formal verification project covering the SNARK
|
|
203
|
+
primitives beneath PQ aggregation.
|
|
204
|
+
|
|
205
|
+
## Further reading
|
|
206
|
+
|
|
207
|
+
- [pq.ethereum.org](https://pq.ethereum.org/) — EF Post-Quantum team, roadmap and FAQ
|
|
208
|
+
- [Post-quantum cryptography on Ethereum](https://ethereum.org/roadmap/security/quantum-resistance/) — ethereum.org
|
|
209
|
+
- [leanEthereum GitHub organisation](https://github.com/leanEthereum)
|
|
210
|
+
- [lean consensus roadmap](https://leanroadmap.org/) and [strawmap.org](https://strawmap.org/)
|
|
211
|
+
- [Securing Elliptic Curve Cryptocurrencies Against Quantum](https://quantumai.google/static/site-assets/downloads/cryptocurrency-whitepaper.pdf) — Google × EF
|
|
212
|
+
- [EIP-8141 implementation tracker](https://github.com/ethereum/execution-specs/issues/2829)
|
|
213
|
+
- [EIP-8141 discussion](https://ethereum-magicians.org/t/eip-8141-frame-transaction/27617) — Fellowship of Ethereum Magicians
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: What w3pk needs for SIWE login on Wulong
|
|
3
|
+
description: Changes w3pk needs so a UI can sign in to Wulong with SIWE, found while hardening Wulong's SIWE verification.
|
|
4
|
+
date: 2026-09-29
|
|
5
|
+
locale: en_US
|
|
6
|
+
author: Julien Béranger
|
|
7
|
+
model: Claude Opus 5.5
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
[Wulong](https://github.com/w3hc/wulong) is hardening its [SIWE](https://eips.ethereum.org/EIPS/eip-4361) verification in [issue #23](https://github.com/w3hc/wulong/issues/23). Once it lands, Wulong verifies every sign-in with the [siwe](https://github.com/spruceid/siwe) library (v3.0.0) and enforces:
|
|
13
|
+
|
|
14
|
+
- **Domain:** the message's `domain` must be one Wulong is configured to accept, which is the UI's host (with its port).
|
|
15
|
+
- **Nonce:** it must be one Wulong issued through `POST /auth/nonce`, tied to the address that signs. It is used up on the first attempt and lasts 5 minutes.
|
|
16
|
+
- **Time:** `Issued At` must fall between when the nonce was issued and now. siwe also enforces `Expiration Time` and `Not Before`.
|
|
17
|
+
|
|
18
|
+
A UI built on w3pk should be able to sign in against these rules. One bug blocks that, and one point needs checking.
|
|
19
|
+
|
|
20
|
+
## 1. Fix `createSiweMessage` when there is no statement
|
|
21
|
+
|
|
22
|
+
**Bug.** [`createSiweMessage`](src/siwe/index.ts#L90) writes one blank line between the address and `URI:` when `statement` is omitted. [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361#message-format) requires two, and the blank line after the statement slot is always there. siwe v3's parser rejects the w3pk output.
|
|
23
|
+
|
|
24
|
+
Tested against siwe 3.0.0:
|
|
25
|
+
|
|
26
|
+
| Message | Result |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| w3pk, no statement | rejected: `invalid message: max line number was 7` |
|
|
29
|
+
| w3pk, with statement | accepted |
|
|
30
|
+
| siwe's own format, no statement | accepted |
|
|
31
|
+
|
|
32
|
+
Until this is fixed, a w3pk UI can sign in to Wulong (or any other server using siwe) only if it passes a `statement`.
|
|
33
|
+
|
|
34
|
+
**Fix.** Always write the blank line that follows the statement slot:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
message += `${params.address}\n`;
|
|
38
|
+
message += `\n`;
|
|
39
|
+
if (params.statement) {
|
|
40
|
+
message += `${params.statement}\n`;
|
|
41
|
+
}
|
|
42
|
+
message += `\n`;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
w3pk's [`parseSiweMessage`](src/siwe/index.ts) already reads this format: an empty line where the statement would be is skipped.
|
|
46
|
+
|
|
47
|
+
**Done when.**
|
|
48
|
+
|
|
49
|
+
- A message built without a statement is byte-identical to the output of `new SiweMessage({...}).prepareMessage()` for the same fields.
|
|
50
|
+
- `test/siwe.test.ts` covers this with a test that builds a message with and without a statement, parses each with siwe and gets it back unchanged. That needs siwe as a dev dependency.
|
|
51
|
+
|
|
52
|
+
## 2. Check SIWE signing in PRIMARY mode
|
|
53
|
+
|
|
54
|
+
**Not verified yet.** [`getAddress('PRIMARY')`](src/core/sdk.ts#L590) returns the address derived from the passkey's [P-256](https://eips.ethereum.org/EIPS/eip-7951) public key. [`signMessage`](src/core/sdk.ts#L731), however, always signs with a key derived from the mnemonic through `getOriginSpecificAddress`, whatever the mode. A SIWE message built with the PRIMARY address and signed with `signMessage(..., { mode: 'PRIMARY', signingMethod: 'SIWE' })` would then carry a signature that does not recover to the address in the message. Wulong, and any server using siwe, would reject it.
|
|
55
|
+
|
|
56
|
+
A P-256 [WebAuthn](https://www.w3.org/TR/webauthn-3/) signature, as `signMessageWithPasskey` produces, isn't a plain [EIP-191](https://eips.ethereum.org/EIPS/eip-191) signature from an externally owned account either. siwe can't verify it without an [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) contract on chain.
|
|
57
|
+
|
|
58
|
+
**Done when**, either way:
|
|
59
|
+
|
|
60
|
+
- `signMessage` rejects `mode: 'PRIMARY'` with `signingMethod: 'SIWE'` with a clear error, and the docs say SIWE needs STANDARD, STRICT or YOLO; or
|
|
61
|
+
- a test shows that a PRIMARY-mode SIWE signature recovers to `getAddress('PRIMARY')`.
|
|
62
|
+
|
|
63
|
+
## 3. Show the server nonce flow in the SIWE example
|
|
64
|
+
|
|
65
|
+
[`examples/siwe-login.ts`](examples/siwe-login.ts) makes the nonce with `generateSiweNonce()` on the client. Wulong issues nonces on the server and ties each to an address, which is also what EIP-4361 recommends. An example of the flow Wulong expects would help UI builders:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const address = await w3pk.getAddress('STANDARD');
|
|
69
|
+
|
|
70
|
+
const { nonce } = await fetch(`${WULONG_URL}/auth/nonce`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ address }),
|
|
74
|
+
}).then((r) => r.json());
|
|
75
|
+
|
|
76
|
+
const message = createSiweMessage({
|
|
77
|
+
domain: window.location.host,
|
|
78
|
+
address,
|
|
79
|
+
uri: window.location.origin,
|
|
80
|
+
version: '1',
|
|
81
|
+
chainId: 1,
|
|
82
|
+
nonce,
|
|
83
|
+
issuedAt: new Date().toISOString(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const { signature } = await w3pk.signMessage(message, {
|
|
87
|
+
mode: 'STANDARD',
|
|
88
|
+
signingMethod: 'SIWE',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await fetch(`${WULONG_URL}/chest/access/${slot}`, {
|
|
92
|
+
headers: {
|
|
93
|
+
'x-siwe-message': btoa(message),
|
|
94
|
+
'x-siwe-signature': signature,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Points to document next to it:
|
|
100
|
+
|
|
101
|
+
- **Same mode and tag.** Call `getAddress` with the same mode and tag you'll sign with. Addresses are derived per origin, so the address the nonce is tied to must be the one that signs.
|
|
102
|
+
- **`domain`.** Use `window.location.host`, which includes the port. Wulong must list that host in `SIWE_DOMAIN`.
|
|
103
|
+
- **Time limit.** Sign within 5 minutes of getting the nonce. A longer `expirationTime` in the message is allowed, but Wulong still stops accepting the nonce after 5 minutes.
|
|
104
|
+
|
|
105
|
+
## Further reading
|
|
106
|
+
|
|
107
|
+
- [EIP-4361: Sign-In with Ethereum](https://eips.ethereum.org/EIPS/eip-4361)
|
|
108
|
+
- [siwe library](https://github.com/spruceid/siwe)
|
|
109
|
+
- [EIP-55: mixed-case checksum address encoding](https://eips.ethereum.org/EIPS/eip-55) (siwe requires checksummed addresses; w3pk's [ethers](https://docs.ethers.org/v6/) wallets already produce them)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w3pk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "WebAuthn SDK for passwordless authentication, encrypted wallets, ERC-5564 stealth addresses, and zero-knowledge proofs",
|
|
5
5
|
"author": "Julien Béranger",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"@types/node": "^25.9.2",
|
|
88
88
|
"@types/qrcode": "^1.5.5",
|
|
89
89
|
"circomlib": "^2.0.5",
|
|
90
|
+
"siwe": "3.0.0",
|
|
90
91
|
"tsup": "^8.5.1",
|
|
91
92
|
"tsx": "^4.22.4",
|
|
92
93
|
"typescript": "^6.0.3"
|
|
@@ -111,7 +112,7 @@
|
|
|
111
112
|
"build:zk": "npm run build && npm run compile:circuits",
|
|
112
113
|
"compile:circuits": "node scripts/compile-circuits.js",
|
|
113
114
|
"dev": "tsup --watch",
|
|
114
|
-
"test": "tsx test/test.ts && tsx test/comprehensive.test.ts && tsx test/backup.test.ts && tsx test/social-recovery.test.ts && tsx test/recovery-education.test.ts && tsx test/zk/zk.test.ts && tsx test/nft-ownership.test.ts && tsx test/chainlist.test.ts && tsx test/eip7702.test.ts && tsx test/external-wallet.test.ts && tsx test/erc5564.test.ts && tsx test/zk/key-stretching.test.ts && tsx test/username-encoding.test.ts && tsx test/username-validation.test.ts && tsx test/origin-derivation.test.ts && tsx test/build-hash.test.ts && tsx test/credential-checking.test.ts && tsx test/webauthn-native.test.ts && tsx test/persistent-session.test.ts && tsx test/sign-message.test.ts && tsx test/siwe.test.ts && tsx test/requirereauth.test.ts && tsx test/eip7951.test.ts && tsx test/send-transaction.test.ts && tsx test/eip1193-provider.test.ts && tsx test/mlkem.test.ts",
|
|
115
|
+
"test": "tsx test/test.ts && tsx test/comprehensive.test.ts && tsx test/backup.test.ts && tsx test/social-recovery.test.ts && tsx test/recovery-education.test.ts && tsx test/zk/zk.test.ts && tsx test/nft-ownership.test.ts && tsx test/chainlist.test.ts && tsx test/eip7702.test.ts && tsx test/external-wallet.test.ts && tsx test/erc5564.test.ts && tsx test/zk/key-stretching.test.ts && tsx test/username-encoding.test.ts && tsx test/username-validation.test.ts && tsx test/origin-derivation.test.ts && tsx test/build-hash.test.ts && tsx test/credential-checking.test.ts && tsx test/webauthn-native.test.ts && tsx test/persistent-session.test.ts && tsx test/local-storage.test.ts && tsx test/sign-message.test.ts && tsx test/siwe.test.ts && tsx test/requirereauth.test.ts && tsx test/eip7951.test.ts && tsx test/send-transaction.test.ts && tsx test/eip1193-provider.test.ts && tsx test/mlkem.test.ts && tsx test/login-missing-wallet.test.ts && tsx test/backup-status.test.ts",
|
|
115
116
|
"test:basic": "tsx test/test.ts",
|
|
116
117
|
"test:comprehensive": "tsx test/comprehensive.test.ts",
|
|
117
118
|
"test:backup": "tsx test/backup.test.ts",
|