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.
@@ -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.2",
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",