build: freenet-stdlib 0.6→0.8 upgrade + delegate/room-contract migration - #394
Conversation
…ration (WIP, do not merge) WIP dogfood of the stdlib 0.6.0 -> 0.8.2 upgrade to prove room + delegate data survives before ever touching the shared River contract. NOT FOR MERGE as-is (uses a [patch.crates-io] to a local stdlib checkout pending the 0.8.3 release, and has NOT been published/verified end-to-end in the browser). What this does: - Bump freenet-stdlib 0.6.0 -> 0.8.2 (workspace), patched to the local checkout until the leak-fix 0.8.3 is on crates.io (leak fix is WebApi-only and does not affect contract/delegate WASM bytes). - Fix the one real 0.8 API break: DelegateRequest::DEFAULT_CIPHER/DEFAULT_NONCE were removed (they were a world-known XChaCha20 key/nonce). Replace with a random 32-byte cipher + 24-byte nonce generated once per install and persisted in localStorage. NOTE: a stdlib-0.8 node IGNORES the client cipher (it derives a per-delegate DEK from the node KEK via HKDF, freenet-core #4140/#4143) and decrypts pre-0.8 secrets via its LEGACY_DEFAULT_CIPHER fallback, so old-secret decryption is the node's job, not River's. - Register the last stdlib-0.6 generation so the 0.8 build migrates FROM it: * legacy_delegates.toml V25 = 8bb5d06f... (delegate_key 3e0e0737...) * common/legacy_room_contracts.toml V25 = 58a5e73c... - Rebuild room-contract + chat-delegate WASM against 0.8 (new keys): * room_contract.wasm 58a5e73c -> 18227bb6 * chat_delegate.wasm 8bb5d06f -> f4cea6bf (delegate_key 3e0e0737 -> da2ce32f) Migration wiring: River's bespoke probe machinery (fire_legacy_migration_request + #292 room backward-probe) is kept as-is; freenet-migrate 0.1.0 has no WS-driver equivalent for it, so a full crate swap is out of scope this pass. The requirement met here is correctness: the 0.6 legacy hashes are registered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
Live verification: room-contract migration 0.6→0.8 WORKS on a real 0.8.2 nodeRan a riverctl-driven room-contract migration against an isolated Keys derived (base58):
What was proven:
Bug found (pre-existing on main, NOT a regression): the in-place upgrade path ( Still NOT verified live: the browser delegate migration round-trip (room list / per-room signing keys / DM history) — delegate migration is browser-only; riverctl doesn't touch the chat delegate. The delegate side is verified at the static level (ABI import/signature parity 0.6↔0.8, V25 compiled into [AI-assisted - Claude] |
Replace the `[patch.crates-io] freenet-stdlib = { path = ... }` dogfood
override with a clean `freenet-stdlib = "0.8.3"` (now on crates.io) across
the workspace, and update Cargo.lock to resolve 0.8.3 from the registry
(checksum 2ce69f7b...). No other transitive deps change.
Verified: published freenet-stdlib 0.8.2 and 0.8.3 produce BYTE-IDENTICAL
contract/delegate WASM (room 74f3dff1, delegate d91e33fc). The 0.8.3 diff
is confined to the browser WebApi client (net feature) plus a dead-code
clippy cleanup in contract_interface/encoding.rs; the leak fix does NOT
touch contract-reachable code. river-ui (wasm32), riverctl, and river-core
all compile clean against 0.8.3.
BLOCKER (not resolved here, pre-existing on this branch): the committed
dogfood WASM (room 18227bb6 / delegate f4cea6bf / delegate_key da2ce32f)
is NOT reproducible — a canonical `cargo make sync-wasm` (toolchain 1.94.1,
--locked, against 0.8.2 OR 0.8.3) produces room 74f3dff1 / delegate
d91e33fc / delegate_key b61dc292 instead. Before republish the WASM must be
regenerated via sync-wasm and the room+delegate migration round-trips
re-verified against the real shippable keys. Committed WASM left untouched
(restored) pending that decision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
… stale artifact) The previously-committed room_contract/chat_delegate WASM on this branch (room 18227bb6…, delegate f4cea6bf…, delegate_key da2ce32f…) was a non-reproducible, dogfood-local artifact: a canonical build does not reproduce it, so CI check-cli-wasm / the reproducibility gate would go red. Regenerated via the canonical co-build path (cargo make sync-wasm) with freenet-stdlib = 0.8.3 (crates.io, no patch), toolchain 1.94.1, --locked. Verified reproducible: two independent from-scratch builds are byte-identical. room code_hash (b3sum) : 74f3dff1c3c2f4ef89e4c93ee45ddb62952df22161450a905c2784c1facf6740 delegate code_hash : d91e33fcf47b4863b996fbf6bb8d782b2250a0e5037fb8dc7a030b60ac1c4de1 delegate_key : b61dc292c952f5ec48f870a0f9829090968a7d03669671f619487f0b5aca8633 The stale artifact was never published (no user data on it), so it needs no legacy entry. The migration anchor remains V25 (the real last stdlib-0.6 generation): main's committed delegate b3sum 8bb5d06f… and room b3sum 58a5e73c… both match the V25 entries, so check-migration passes against base. ui/public/contracts/ and cli/contracts/ room WASM kept in sync (check-cli-wasm). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
In production River runs inside the gateway iframe with an opaque origin, so `window.localStorage` throws SecurityError. Both `load_persisted_cipher_material` and `persist_cipher_material` then silently no-op, so the pre-fix `chat_delegate_cipher_material()` generated a FRESH random cipher/nonce on every `set_up_chat_delegate()` call — which fires on every reconnect and sleep/wake — rotating the client-supplied delegate cipher within a single page. Add a process-memory fallback (`IN_MEMORY_CIPHER_MATERIAL`, a `thread_local!` — wasm is single-threaded) used when localStorage is unavailable: the cipher/nonce is generated once and stays stable for the page lifetime. Order is unchanged where storage works — try localStorage first, write-through when available — so a normal top-level origin still gets cross-reload stability; only the storage-denied iframe path changes, from per-call rotation to page-stable. Also: - Correct the doc-comment to describe the real persistence model (localStorage where available, page-stable in-memory otherwise) and to note that 0.8 nodes ignore the client cipher (KEK-derived DEK), so cross-reload rotation is harmless on the 0.8 fleet; the only residual is a pre-0.8 cipher-honoring node. - Fix the legacy migration TOML descriptions: "0.6.0 -> 0.8.2" -> "0.8.3" to match the actual stdlib pin. - Add `cipher_material_is_stable_within_process` test pinning the fix. UI-only change: the cipher code compiles into river-ui, NOT the delegate WASM. `cargo make sync-wasm` reproduces byte-identical artifacts — chat_delegate d91e33fc…, room_contract 74f3dff1…, delegate_key b61dc292… — so migration verification is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
Two follow-ups to the chat-delegate cipher-stability fix: 1. Correct the safety doc-comment. The earlier wording hedged that the pre-0.8 cipher-honoring brick "may be unreachable if the 0.6→0.8 client-API wire is incompatible". Verified (#394) that it is NOT: there is no protocol-version handshake and the `RegisterDelegate` bincode layout is byte-identical from stdlib v0.1.22 through 0.8.3, so a stdlib-0.8 River client CAN connect to and register a delegate on a pre-0.8 node. The residual is therefore reachable at the protocol level; what makes it unlikely is that a cipher-honoring node is freenet-core ≲0.2.58 (pre-#4143/#4144), which the live ~0.2.9x auto-updated fleet almost certainly no longer runs. The doc now also states the fix is partial for that node class: it removes within-page (reconnect/sleep-wake) rotation but not cross-reload rotation in the storage-denied iframe. 2. Bump riverctl 0.1.73 -> 0.1.74 (+ Cargo.lock). The stdlib 0.6→0.8 bump changed room_contract.wasm, and check-wasm-sync fails when the CLI version still matches the crates.io-published one (riverctl embeds the WASM to derive the contract key). This is the check's own prescribed fix; no WASM changes (delegate/room-contract hashes unchanged: chat_delegate d91e33fc…, room_contract 74f3dff1…). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
Blocker 1 resolved: chat-delegate cipher stability in the sandboxed gateway iframeThe bugIn production River runs inside the gateway iframe with an opaque origin, so The fixAdded a process-memory fallback ( Pre-0.8 reachability assessment (the safety justification)On stdlib-0.8 nodes the node ignores the client-supplied cipher/nonce and derives a per-delegate DEK from its own KEK (freenet-core I checked whether a stdlib-0.8 River client can even reach such a node. It can — the residual is not gated out by wire incompatibility:
So the pre-0.8 cipher-honoring case is reachable at the protocol level. What makes it unlikely in practice: a cipher-honoring node is freenet-core ≲ 0.2.58 (stdlib pre-0.8, ~April 2026, before #4143/#4144); every release from ~0.2.60 ignores the client cipher, and the live network runs ~0.2.9x with auto-update. Such an ancient non-updated peer almost certainly no longer exists, but I can't prove from code that none do. Honest scope of the fix: it removes the within-page (reconnect / sleep-wake) rotation, which was the actual regression. In the iframe it does not make the material stable across a full page reload (localStorage still no-ops there), so for a hypothetical pre-0.8 honoring node it reduces rather than fully eliminates the theoretical cross-reload brick. On the entire 0.8 fleet this is moot (client cipher ignored). WASM unchanged (migration verification still holds)The cipher code compiles into
Also in this change
[AI-assisted - Claude] |
Full-tier review complete — GO for republishReproducible canonical build. room Both migrations re-verified against the real shippable keys on an isolated 0.8 node:
External Codex + adversarial cipher-security review: clean. The one finding (cipher rotation in the sandboxed gateway iframe) is fixed via a Legacy-registry audit complete. V25 anchors the last live 0.6 generation; no missing live generation. CI green on Scope notes. Orthogonal pre-existing bug #395 (riverctl in-place upgrade) does not affect fresh-client recovery. Republish is a deliberate, in-the-loop operation: it re-keys the shared contract, old invite URLs die, and the Official room is recreated. [AI-assisted - Claude] |
…fix + contract/delegate re-key) (#32) * chore: register legacy 0.6 contract/delegate hashes before stdlib 0.8.3 bump Records the current freenet-stdlib 0.6.0 delegate (V10) and contract (C5) WASM code hashes in the legacy registries BEFORE the 0.8.3 rebuild re-keys both WASMs. Without this, the UI's backward-probe migration could not find existing sites' state / delegate secrets under the old keys after upgrade, silently orphaning every user's data. delegate V10 code_hash: 4d576358229eed2ea567d8c3275776f584a0a66f41b4b05058644da4d4fc56aa contract C5 code_hash: 8e36f95d436899960cf31afd76586837a3bf9f91483dcd09d26483ad02823c53 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG * feat: bump freenet-stdlib 0.6.0 -> 0.8.3 (inbound-WS FileReader leak fix) Delivers Delta the inbound-WebSocket FileReader memory-leak fix that River already shipped: Delta's UI uses stdlib's `net` feature, which had the same leak (every inbound WS message leaked a FileReader). Compile break fixed: stdlib 0.8 removed the world-known `DelegateRequest::DEFAULT_CIPHER` / `DEFAULT_NONCE` constants the UI passed to `RegisterDelegate`. Replaced with a per-install random cipher/nonce (persisted to localStorage where available, page-stable in-memory fallback in the sandboxed gateway iframe), mirroring River's field-proven fix (freenet/river#394). On a stdlib-0.8 node the value is ignored server-side (the node derives the DEK from its own KEK, with a legacy fallback for pre-0.8 world-known-cipher secrets), so this is confidentiality hardening plus supplying the now-required fields — not a migration dependency. The contract and delegate WASM compiled against 0.8.3 with NO source changes (the delegate uses the high-level `ctx.set_secret`/`get_secret` API, which abstracts the cipher). WASM re-key (content-addressed identity changed on rebuild): delegate 0.6 4d576358.. -> 0.8 06b7e80f.. (registered as legacy V10) contract 0.6 8e36f95d.. -> 0.8 7f81b4d6.. (registered as legacy C5) Both 0.6 predecessors were registered in the prior commit so the UI's backward-probe migration carries existing sites' state and delegate secrets forward. WASMs build byte-identically (verified via two forced clean rebuilds; toolchain pinned at 1.94.1, same as River). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This branch dogfoods the freenet-stdlib 0.6.0 → 0.8.2 upgrade to prove that room + delegate data survives before we ever touch the shared production River contract (
raAqMhMG7KUpXBU2SxgCQ3Vh4PYjttxdSWd9ftV7RLv). It is not for merge as-is: it pins stdlib via a[patch.crates-io]to a local checkout (so CI will fail to resolve deps — expected), and the full browser delegate round-trip was not executed live in this pass.What changed
stdlib 0.6.0 → 0.8.2 (workspace
Cargo.toml), patched to the localfreenet-stdlibcheckout until the leak-fix 0.8.3 is published to crates.io. The leak fix is WebApi(browser-client)-only and does not affect the contract/delegate WASM bytes this upgrade re-keys, so the patched 0.8.2 is byte-equivalent to what we'll ship on for the migration surface.Fixed the one real 0.8 API break (
DEFAULT_CIPHER/DEFAULT_NONCE) inui/src/components/app/chat_delegate.rs. stdlib 0.8 removed these constants — they were a world-known XChaCha20 key + nonce. Replaced with a random 32-byte cipher + 24-byte nonce generated once per install and persisted in localStorage.register_delegatederives a per-delegate DEK from the node's own KEK via HKDF, and decrypts pre-0.8 secrets (written under the old world-known cipher) via a built-inLEGACY_DEFAULT_CIPHERfallback (decrypt_secret_blobTier-3). So old-secret decrypt/re-encrypt is the node's job, not River's. Persisting a per-install random value is the correct, forward-compatible choice: harmless on 0.8 nodes (discarded), and a per-install secret (not world-known) on any older node that still honors it.Registered the last stdlib-0.6 generation so the 0.8 build migrates from it:
legacy_delegates.tomlV25 =8bb5d06f…(delegate_key3e0e0737…)common/legacy_room_contracts.tomlV25 =58a5e73c…Rebuilt room-contract + chat-delegate WASM against 0.8 (re-keys):
room_contract.wasm58a5e73c…e47c5018227bb6…a7b151chat_delegate.wasm8bb5d06f…51cc8df4cea6bf…cf24b483e0e0737…10eda80da2ce32f…bc65eb61Migration wiring decision
River's bespoke probe machinery (
fire_legacy_migration_request+ the #292 room backward-probe incommon/src/migration.rs) is kept as-is.freenet-migrate0.1.0 is pure/synchronous and has no WebSocket-driver equivalent for River's app-side probe loop, so a full crate swap is out of scope this pass. The requirement met here is correctness: the 0.6 legacy hashes are registered so the existing (24-prior-bumps-proven) probe machinery carries data forward.freenet-migrate's clean-fit surface (build-time codegen + hash-guard + key derivation) is byte-identical to River's derivation (proven by the crate's owncodegen_stdlib_consistencytest + River delegating to stdlib'sContractKey::from_params), and is a good candidate for a later, separate adoption PR.Verified
--locked, reproducible), river-ui compiles (cargo check, wasm target).migration_test+room_contract_migration_test, 4+4), validating the V25 entries anddelegate_key == BLAKE3(code_hash).get_secret/set_secret/remove_secret/get_secret_len/logger.info); the room-contract imports match too. So the installed 0.8.2 node can execute the 0.6 WASMs — the migration's core requirement holds.freenet-migrate0.1.0 test suite green against local stdlib 0.8.2.NOT verified live (yet)
Live riverctl-driven room-contract migration test against a real 0.8 node is running separately; result to follow in a comment.
🤖 Generated with Claude Code
[AI-assisted - Claude]