Skip to content

fix: close rust and fuse scope-exit read-revocation bypasses#607

Merged
FSM1 merged 46 commits into
mainfrom
feat/rust-and-fuse-rotation-revocation-soundness
Jul 11, 2026
Merged

fix: close rust and fuse scope-exit read-revocation bypasses#607
FSM1 merged 46 commits into
mainfrom
feat/rust-and-fuse-rotation-revocation-soundness

Conversation

@FSM1

@FSM1 FSM1 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 74 (v2.0 M4 closeout) closes the remaining scope-exit read-revocation bypasses on the Rust/desktop side so the revocation guarantee holds end-to-end. The rotation engine now surfaces every rotated node's new read key (not just the grant root), all intermediate FUSE inodes are refreshed on rotation, the desktop grant-re-mint seam is wired (retained recipients keep access, revoked ones are cut off), and WinFsp overwrite-rename is dest-gated with fuser ordering parity.

What changed (by success criterion)

  • SC1 — deep key surfacing + inode refresh: RotateReadResult.rotatedNodes widened to carry every rotated node's read key in both engines (crates/sdk/src/rotation/engine.rs, packages/sdk-core/src/rotation/engine.ts); FUSE refresh_rotated_inode_read_keys refreshes every rotated inode (Root/Folder/File) before relink, closing the stale-key reseal path (crates/fuse/src/write_ops/grant_scope.rs).
  • SC2 — grant re-mint seam: FuseRotationDeps drives collect_sent_shares / update_grant / revoke_share via a new RotationTransport seam on ApiClientTransport (crates/api-client/src/{client,shares}.rs, crates/fuse/src/write_ops/rotation_deps.rs) — retained recipients re-wrapped under the NEW read key, revoked ones cut off (revocation-by-absence).
  • SC3 — WinFsp dest-gate: overwrite-rename dest-gated with D-15d validate→gate→mutate ordering matching the fuser path (crates/fuse/src/platform/windows/write_ops.rs).

Verification

Local gates (all green):

  • Unit/integration: Rust cargo test -p cipherbox-sdk + -p cipherbox-fuse, sdk-core rotation 370, plus workspace cargo check green at every wave boundary.
  • SDK-E2E gate: 105/106 (16 suites) against a live client→API round-trip — all rotation/key-lifecycle suites green (rotation-crash-safety 6/6, write-chain-rotation, share-operations, ipns-consistency/publish-gate, read-chain-navigation, data-integrity). The single failure is the pre-existing tee-republish lease-renewer infra dependency (host tee-worker epoch-mismatch), unrelated to this phase's rotation path.
  • Secure: 74-SECURITY.md SECURED, threats_open: 0 (16/16 closed).
  • Crypto/privacy review: no CRITICAL/HIGH defects; D-09 terminal-owner zeroization, deep-refresh ordering, revocation invariant, and read-key-only wire DTOs all sound. See .planning/security/REVIEW-2026-07-11-phase74-rotation.md.
  • Built-in security review: no qualifying vulnerabilities.

CI-gated legs (the final runtime gate — run by this PR)

Two success-criteria legs cannot compile/run on a macOS host and are verified by this PR's CI, not locally. Until they pass, treat runtime verification of these as pending:

  1. Cargo Check & Test (Windows) — the SC3 WinFsp code (#[cfg(windows)]) only compiles under the winfsp feature on Windows CI.
  2. Desktop E2E Tests (macOS/Windows/Linux matrix) — the SC1/SC2/SC3 live FUSE/WinFsp-mount acceptance leg (shared-scope-exit-rotation.mts).

The code for both is source-verified line-by-line against the fuser-path parity; these are runtime confirmations. (Recorded in 74-UAT.md / 74-VERIFICATION.md required_ci_gates.)

Follow-ups logged (todos, not blockers)

  • .planning/todos/pending/2026-07-11-remint-trusts-server-recipient-pubkey-binding.md — MEDIUM: grant re-mint trusts the server for the recipient pubkey binding (inherited, newly reachable on desktop); needs a design decision on binding verification.
  • .planning/todos/pending/2026-07-11-ts-rotatednodes-defensive-copy-parity.md — LOW/latent: TS rotatedNodes stores read keys by reference; add defensive copies to match Rust's .clone() and remove a future-zeroing hazard.

Review notes

  • CodeRabbit CLI (--base main, committed): 8 findings, all on internal .planning/ tracking docs (completion-marker phrasing re: the CI-gated legs above), 0 on code — dismissed; the authoritative caveat is stated here and in 74-UAT.md.
  • Commit subjects are conventional; .planning/ bookkeeping rides this PR per GSD convention.

🤖 Generated with Claude Code

https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL

Summary by CodeRabbit

  • Bug Fixes
    • Refreshed post-rotation access keys for every rotated node (including intermediate folders, file nodes, and crash-recovery/dirty-resume paths).
    • Fixed desktop rotation grant re-mint/update/revoke flows used during share remint.
    • Corrected Windows overwrite-rename to apply destination scope-exit validation before allowing the rename to proceed.
    • Strengthened revoked vs retained recipient behavior in desktop end-to-end rotation scenarios.
  • Tests
    • Added deep Rust/TypeScript parity coverage for per-node rotated key reporting.
    • Expanded desktop end-to-end acceptance to cover deeper delete and overwrite-rename destination-gate scenarios.

FSM1 and others added 30 commits July 11, 2026 02:31
Resolve plan-checker blocker + 2 warnings on the Phase 74 draft plans:

- 74-05: route grant-query ops through a RotationTransport seam extension
  (collect_sent_shares/update_grant/revoke_share) instead of an ungeneric
  self.transport.api reach-around; keeps grant_scope.rs and FuseRotationDeps::new
  untouched, so wave 2 (74-02/74-03/74-05) has zero files_modified overlap.
- 74-01/74-02/74-03/74-04 Task 1: tighten RED-confirmation <automated> blocks to
  the negation form so an unexpected pass genuinely fails (no trailing echo mask).
- 74-VALIDATION.md: sync with authored plans (status ready, nyquist_compliant true,
  real per-task verification map, sign-off checked).

plan-checker: VERIFICATION PASSED (iteration 2). SC1/SC2/SC3 all covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: f8220eed0534
Entire-Checkpoint: 2ed267d94d2e
Adds rotate_read_surfaces_every_rotated_node_key_for_a_deep_tree,
seeding a 3-node tree (grant-root -> folderB -> fileC) and asserting
RotateReadResult.rotated_nodes contains every level's post-rotation
key, keyed by ipns_name. Fails to compile until Task 2 adds the field
(RED confirmed).

Entire-Checkpoint: ff5d15794cc5
Adds RotatedNodeKey and RotateReadResult.rotated_nodes (additive,
existing read_key/generation/sequence_number root-convenience fields
kept), keyed by ipns_name per the LOCKED cross-language contract
(parity twin is plan 74-02). Populated at three hooks proven to fire
for every committed node:

- the root commit branch (rotate_read_from_node_inner)
- the BFS child commit branch (same function's walk loop)
- repair_dirty_node's crash-resume repair path (its recovered_key IS
  the node's current valid post-rotation key, readily available, so
  folded in per RESEARCH Open Question 1 rather than deferred)

CommittedRotation gains no ipns_name field (stays host-agnostic);
ipns_name is threaded in at each call site where it is already in
scope. No zeroing of key material inside the engine (D-09).

Deep-tree test from the prior RED commit now passes; scoped
cipherbox-sdk rotation::engine:: suite is green (27/27), no
regression to existing call sites.

Entire-Checkpoint: d72fb57614e5
- Add raw-TCP capturing mock server (mirrors delete.rs's spawn_mock_rotation_server pattern) to shares.rs test module
- Tests assert update_grant PATCHes /shares/:shareId/grant with encryptedReadKey+rootGeneration only, and revoke_share DELETEs /shares/:shareId
- Both non-2xx paths assert ApiError::ApiResponse with op-name-prefixed message
- RED: module fails to compile — update_grant/revoke_share do not exist yet

Entire-Checkpoint: e6bc166427be
- ApiClient::authenticated_patch/authenticated_delete mirror authenticated_post's auth-header + reqwest builder shape
- shares::update_grant PATCHes /shares/:shareId/grant with UpdateGrantRequest (encryptedReadKey + rootGeneration only, camelCase); root_generation formatted from u64 to the DTO's numeric string; forwards an already-ECIES-wrapped ciphertext, no key material handled here
- shares::revoke_share DELETEs /shares/:shareId
- Both treat 204 as success and map non-2xx to ApiError::ApiResponse with op-name-prefixed messages
- Task 1 wire tests now pass; scoped api-client suite green (35/35)

Entire-Checkpoint: 3c496f34e462
…unctions plan

Entire-Checkpoint: 1993674c3eaa
Author two WinFsp unit tests mirroring the fuser rename.rs D-15d twins
(rename_enotempty_destination_rejects_before_gate_with_no_rotation_attempt,
rename_overwriting_a_covered_destination_gates_dest_ino_scope_exit), each
asserting the correct rejection status code plus zero partial mutation.
They exercise handle_rename end-to-end via a WinFspContext wrapping the
shared test_support harness (widened from fuse-only to fuse-or-winfsp, with
the fuser-specific CaptureSender/reply_error_code kept fuse-gated). Against
today's unfixed handle_rename ordering these fail closed with the wrong
status code, proving the not-yet-added dest gate is exercised.

Entire-Checkpoint: 8241ef943871
Reorder WinFsp handle_rename to match the fuser rename.rs D-15d pipeline:
the replace_if_exists collision check stays first and unchanged; the
STATUS_DIRECTORY_NOT_EMPTY destination-replacement validation now runs
BEFORE the source scope-exit gate (a doomed rename never triggers a
rotation); the existing source gate is unchanged; a NEW destination
scope-exit gate runs immediately after it, before the previously-ungated
fs.inodes.remove(dest_ino), returning STATUS_ACCESS_DENIED on failure. Uses
the plain (non-coalesced) gate, matching the fuser reference. Closes a
Windows-only overwrite-rename revocation bypass (T-74-09) and advances SC3.

Entire-Checkpoint: 1d6d481fa431
Entire-Checkpoint: 07d2d7569182
- Seed grant-root Folder, intermediate Folder, and File inodes with distinct
  ipns_names and zeroed pre-rotation read_keys
- Build a RotateReadResult.rotated_nodes map with new keys for all three
- Reference the not-yet-existing refresh_rotated_inode_read_keys — genuine
  compile-error RED, confirmed via cargo test
- Export RotatedNodeKey from cipherbox_sdk::rotation (was previously
  crate-internal to engine.rs, needed by grant_scope.rs's test and Task 2)

Entire-Checkpoint: 3d2d751017b3
- Rename refresh_grant_root_read_key to refresh_rotated_inode_read_keys and
  generalize it to loop over RotateReadResult.rotated_nodes, matching each
  entry's ipns_name against every inode with no early return
- Extend the match arms from Root | Folder to Root | Folder | File so file
  inodes (rotated via CRIT-1/mint_file_key_on_rotate) are refreshed too,
  closing a related staleness gap
- Update the rotate_read_on_scope_exit call site to the new signature
- Move the RotatedNodeKey import into the test module (cfg(test)-only use)
  to avoid an unused-import warning in the non-test build

Entire-Checkpoint: 503fec2268bc
Extend the RotationTransport seam with collect_sent_shares/update_grant/
revoke_share, implement them on the test FakeTransport, and add
FakeTransport-driven tests for FuseRotationDeps::query_grants_rooted_at/
update_grant/delete_grant. ApiClientTransport does not yet implement the
three new trait methods, so cipherbox-fuse fails to compile - genuine RED
before the GREEN implementation task wires the production adapter.

Entire-Checkpoint: 094dc96e6df9
Implement ApiClientTransport's collect_sent_shares/update_grant/
revoke_share against the 74-04 wire functions, and replace the
FuseRotationDeps ROT-04 no-op defaults with real overrides that
delegate to the transport seam: query_grants_rooted_at filters
collect_sent_shares by root_node_id and hex-decodes
recipient_public_key (0x stripped, 04 prefix kept); update_grant
forwards the already-ECIES-wrapped key without re-wrapping;
delete_grant forwards to revoke_share. grant_scope.rs and
FuseRotationDeps::new are unchanged.

Also routes GET /shares/sent in delete.rs's pre-existing mock
rotation server to an empty page - that test previously relied on
query_grants_rooted_at's no-op default never hitting the network,
and now genuinely exercises the live seam.

Entire-Checkpoint: 2dcc4862be6a
Adds rotateReadFromNode surfaces every rotated node key for a deep tree,
seeding a 3-node root -> folderB -> fileC tree via existing mock helpers
and asserting result.rotatedNodes contains an entry per rotated ipnsName.
Mirrors the Rust 74-01 deep-tree parity test. RED confirmed: fails at
runtime because RotateReadResult has no rotatedNodes field yet.

Entire-Checkpoint: 4fb4a20f759a
Adds RotatedNodeKey type and RotateReadResult.rotatedNodes: Map<string,
RotatedNodeKey> to packages/sdk-core/src/rotation/engine.ts, mirroring the
Rust twin's rotated_nodes widening from plan 74-01 field-for-field
(sequenceNumber kept bigint, matching this file's existing IPNS
sequence-number convention rather than the plan table's literal number).

Populated at the root commit branch and the BFS child commit branch inside
rotateReadFromNode, keyed by ipnsName. Exported via the rotation and
top-level barrels. Deep-tree parity test (Task 1) now green; sdk-core
rotation suite passes 370/370.

Entire-Checkpoint: 7404bd85df73
Extends shared-scope-exit-rotation.mts with a depth>=2 leg (grant-root ->
folderB -> fileC/fileSibling) asserting a revoked recipient cannot decrypt
any node under the grant root at any depth, exercising 74-03's intermediate
Folder/File inode read_key refresh (SC1). Adds a retained-recipient leg
in the same deep tree, polling for a re-minted encryptedReadKey via
FuseRotationDeps::query_grants_rooted_at/update_grant (74-05, SC2) to prove
an active grantee keeps access rather than being wrongly cut. The existing
shallow D-16 legs (Part A/B) are left unchanged.

Also adds tests/desktop-e2e/tsconfig.json (mirrors the web-e2e pattern),
which did not previously exist -- required to typecheck the desktop-e2e
.mts/.ts scripts at all.

Entire-Checkpoint: dc1c247c9a99
Part D of shared-scope-exit-rotation.mts (landed alongside Task 1's
deep/retained-vs-revoked legs in the same file edit, e1c1291) exercises
an overwrite-rename onto a covered destination: a revoked recipient's
stale key must fail post-rename (74-06's WinFsp handle_rename destination
scope-exit gate, SC3) while a retained recipient's re-minted key still
decrypts. Confirmed via grep that no equivalent rename-overwrite leg
already existed in tests/desktop-e2e/scripts (RESEARCH A3) before adding
this one to the same script.

Updates run-all.sh/run-all.ps1's Step 8 comments (the invocation itself
was already correct and unchanged) to document the new Part C/D coverage,
so the WinFsp-authoritative proof for SC3 is clearly signposted on the
Windows CI leg.

Entire-Checkpoint: c1cd6cb3d525
@github-actions github-actions Bot added release:sdk-core:feat Minor version bump (new feature) for sdk-core release:desktop:fix Patch version bump (bug fix) for desktop release:sdk:fix Patch version bump (bug fix) for sdk release:web:fix Patch version bump (bug fix) for web release:tee-worker:fix Patch version bump (bug fix) for tee-worker labels Jul 11, 2026
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Release Preview

Package Bump Label Source
api minor release:api:feat Direct (feat commit)
cipherbox-fuse minor release:cipherbox-fuse:feat Direct (feat commit)
cipherbox-sdk minor release:cipherbox-sdk:feat Direct (feat commit)
desktop minor release:desktop:fix Cascade (cipherbox-sdk minor)
sdk patch release:sdk:fix Direct (fix commit)
sdk-core minor release:sdk-core:feat Direct (feat commit)
tee-worker patch release:tee-worker:fix Cascade (sdk-core minor)
web minor release:web:fix Cascade (sdk-core minor)

Cascade Details

  • cipherbox-sdk minor -> desktop patch (direct dependency)
  • sdk-core minor -> web patch (direct dependency)
  • sdk-core minor -> tee-worker patch (direct dependency)

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR closes Rust, FUSE, and desktop read-revocation gaps around scope-exit rotation. The main changes are:

  • Per-node rotated read keys are surfaced by the Rust and TypeScript rotation engines.
  • FUSE refreshes rotated Root, Folder, and File inode read keys from the rotation result.
  • Desktop grant re-minting now updates retained recipients and removes revoked shares.
  • WinFsp overwrite-rename now validates and gates destinations before mutation.
  • Desktop end-to-end and rotation tests were expanded for the new revocation paths.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
crates/sdk/src/rotation/engine.rs Adds per-node rotated key surfacing and returns repaired descendant keys on dirty resume.
packages/sdk-core/src/rotation/engine.ts Adds rotatedNodes and records repaired dirty descendants in the returned map.
crates/fuse/src/write_ops/grant_scope.rs Refreshes matching in-memory inode read keys for every rotated Root, Folder, and File.
crates/fuse/src/platform/windows/write_ops.rs Validates self-replace, kind mismatch, and destination scope-exit cases before overwrite mutation.

Reviews (8): Last reviewed commit: "fix(74): make access-token TTL configura..." | Re-trigger Greptile

Comment thread crates/fuse/src/platform/windows/write_ops.rs
Comment thread packages/sdk-core/src/rotation/engine.ts
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.30435% with 62 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.55%. Comparing base (bd8c1e0) to head (931fa71).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
crates/fuse/src/write_ops/rotation_deps.rs 83.33% 35 Missing ⚠️
crates/api-client/src/shares.rs 93.82% 11 Missing ⚠️
crates/fuse/src/write_ops/grant_scope.rs 91.46% 7 Missing ⚠️
crates/sdk/src/rotation/engine.rs 96.19% 7 Missing ⚠️
crates/api-client/src/client.rs 93.10% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #607      +/-   ##
==========================================
+ Coverage   70.12%   77.55%   +7.43%     
==========================================
  Files         157      197      +40     
  Lines       15403    32657   +17254     
  Branches     1952     1952              
==========================================
+ Hits        10801    25327   +14526     
- Misses       4348     7076    +2728     
  Partials      254      254              
Flag Coverage Δ
api 84.74% <100.00%> (+0.03%) ⬆️
api-client 84.74% <100.00%> (+0.03%) ⬆️
core 84.74% <100.00%> (+0.03%) ⬆️
crypto 84.74% <100.00%> (+0.03%) ⬆️
desktop 21.41% <ø> (-10.03%) ⬇️
rust 79.10% <91.00%> (?)
sdk 84.74% <100.00%> (+0.03%) ⬆️
sdk-core 84.74% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
apps/api/src/auth/services/token.service.ts 97.77% <100.00%> (+0.05%) ⬆️
crates/fuse/src/lib.rs 98.38% <ø> (ø)
crates/fuse/src/test_support.rs 100.00% <ø> (ø)
crates/fuse/src/write_ops/implementation/delete.rs 89.71% <100.00%> (ø)
packages/sdk-core/src/rotation/engine.ts 80.36% <100.00%> (+0.38%) ⬆️
crates/api-client/src/client.rs 76.78% <93.10%> (ø)
crates/fuse/src/write_ops/grant_scope.rs 89.82% <91.46%> (ø)
crates/sdk/src/rotation/engine.rs 89.06% <96.19%> (ø)
crates/api-client/src/shares.rs 91.19% <93.82%> (ø)
crates/fuse/src/write_ops/rotation_deps.rs 84.31% <83.33%> (ø)

... and 89 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 688156af5d34
Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread crates/sdk/src/rotation/engine.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (2)
crates/fuse/src/write_ops/grant_scope.rs (1)

575-600: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Swap loop order for O(inodes) instead of O(rotated_nodes × inodes).

Iterating all inodes once per rotated node is O(R×I). Since each inode's ipns_name should be unique, a single pass over inodes with an O(1) rotated_nodes.get(...) lookup does the same job in O(I) — meaningfully cheaper for deep-tree rotations with many rotated nodes and/or a large materialized inode table.

♻️ Proposed loop-order swap
 fn refresh_rotated_inode_read_keys(inodes: &mut InodeTable, result: &RotateReadResult) {
-    for rotated in result.rotated_nodes.values() {
-        for inode in inodes.inodes.values_mut() {
-            match &mut inode.kind {
-                InodeKind::Root { ipns_name, read_key, .. }
-                | InodeKind::Folder { ipns_name, read_key, .. }
-                | InodeKind::File { ipns_name, read_key, .. }
-                    if ipns_name.as_str() == rotated.ipns_name.as_str() =>
-                {
-                    read_key.copy_from_slice(rotated.read_key.as_slice());
-                }
-                _ => {}
-            }
-        }
-    }
+    for inode in inodes.inodes.values_mut() {
+        match &mut inode.kind {
+            InodeKind::Root { ipns_name, read_key, .. }
+            | InodeKind::Folder { ipns_name, read_key, .. }
+            | InodeKind::File { ipns_name, read_key, .. } => {
+                if let Some(rotated) = result.rotated_nodes.get(ipns_name.as_str()) {
+                    read_key.copy_from_slice(rotated.read_key.as_slice());
+                }
+            }
+            _ => {}
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fuse/src/write_ops/grant_scope.rs` around lines 575 - 600, Update
refresh_rotated_inode_read_keys to iterate over inodes once, extract each
inode’s ipns_name and read_key across the existing Root, Folder, and File
variants, then use an O(1) lookup in result.rotated_nodes to copy the matching
rotated read key. Preserve the existing no-op behavior for unmatched inode kinds
or names.
crates/api-client/src/client.rs (1)

94-113: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Drop the access-token read lock before awaiting the HTTP request.

Both methods hold the RwLock read guard through send().await, which can block set_access_token and clear_access_token for the full request timeout. Clone the token before building the request, then release the guard before .send().await; apply the same pattern consistently to the existing authenticated helpers.

Also applies to: 116-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-client/src/client.rs` around lines 94 - 113, Update
authenticated_patch and the existing authenticated helper methods to clone the
access token while holding the RwLock read guard, then release the guard before
building and sending the HTTP request. Ensure bearer authentication uses the
cloned token and no lock guard remains alive across send().await.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
@.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-01-PLAN.md:
- Around line 33-41: Update the RotatedNodeKey.sequenceNumber entry in both
parity tables:
.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-01-PLAN.md
lines 33-41 and
.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-02-PLAN.md
lines 34-40. Document the TypeScript representation as bigint, or explicitly
describe the language-specific representation, so both locked contracts match
the implementation’s safe 64-bit sequence handling.

In
@.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-04-PLAN.md:
- Around line 93-103: Constrain root_generation in the proposed update_grant API
to the server-supported 0..=i64::MAX range by using a bounded type or validating
before formatting and sending it. Update the corresponding contract statement in
.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-04-PLAN.md
lines 93-103 and correct the u64 claim in
.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-04-SUMMARY.md
lines 129-134.

In
@.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-07-PLAN.md:
- Around line 36-39: Align the SC1 acceptance criteria with the executed test:
in .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-07-PLAN.md
lines 36-39, make folderB and fileSibling the authoritative hard decryptability
assertions and treat fileC as informational rather than a required hard
assertion. In
.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-07-SUMMARY.md
lines 36-40, preserve the existing rationale and authoritative assertion
wording; no unrelated changes are needed.

In
@.planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-VERIFICATION.md:
- Around line 1-6: Update the frontmatter status in the verification report,
including the corresponding reports for phases 19–25, from passed to
human_needed so it matches the report status and remains pending until the
Windows and desktop-E2E validations complete.

In @.planning/ROADMAP.md:
- Around line 917-938: Update the Phase 74 progress block in the roadmap to
retain only the final “7/7 plans complete” state, removing the intermediate
executed counts from “6/7” through “1/7” while preserving the completed plan
checklist.

In @.planning/STATE.md:
- Around line 624-625: Update the session checkpoint metadata in STATE.md so the
“Stopped at” entry reflects the actual final plan completed for Phase 74 rather
than 74-02-PLAN.md, while preserving the existing “Last session” timestamp and
completion status.

In `@crates/fuse/src/platform/windows/write_ops.rs`:
- Around line 1102-1105: Update the destination lookup in the rename flow around
find_child so a result equal to source_ino is normalized to None before applying
the replace_if_exists collision check. Preserve ordinary overwrite handling for
other destination inodes and allow same-path or case-only renames to continue
through the normal rename/no-op path without deleting the source inode.

In `@crates/fuse/src/write_ops/rotation_deps.rs`:
- Around line 264-286: The per-node re-mint flow currently refetches sent shares
through query_grants_rooted_at. Add rotation-scoped caching of the
collect_sent_shares result, initialize or populate it once during the rotation,
and have query_grants_rooted_at filter the cached list by root_node_id instead
of calling the transport; preserve existing key parsing and error handling.

In `@packages/sdk-core/src/rotation/engine.ts`:
- Around line 2047-2051: Update the dirty-resume flow around repairDirtyNode so
each repaired node is inserted into the shared rotatedNodes map after resolving
its current record, including its ipnsName, recovered readKeyPrime, generation,
and sequence number. Preserve the existing BFS map instance and add a regression
test confirming dirty-resume callers receive refreshed cached keys for repaired
descendants.

In `@tests/desktop-e2e/scripts/shared-scope-exit-rotation.mts`:
- Around line 321-329: Move the protected try/finally scope to include the
recipient authentication calls around authenticateRecipient and the existing
variables eve, carol, frank, and grace. Store each successfully authenticated
recipient in clearable optional bindings, and ensure the shared finally clears
every assigned private key even when a later authentication rejects.
- Around line 938-947: Reload folderBRef using carolNewGrantRootKey after Carol
receives the rotated grant, then pass the refreshed reference to
deriveChildReadKey before calling canRead. Update the retained-access check
around carolNewFolderBKey while preserving the existing acceptance assertion.

---

Nitpick comments:
In `@crates/api-client/src/client.rs`:
- Around line 94-113: Update authenticated_patch and the existing authenticated
helper methods to clone the access token while holding the RwLock read guard,
then release the guard before building and sending the HTTP request. Ensure
bearer authentication uses the cloned token and no lock guard remains alive
across send().await.

In `@crates/fuse/src/write_ops/grant_scope.rs`:
- Around line 575-600: Update refresh_rotated_inode_read_keys to iterate over
inodes once, extract each inode’s ipns_name and read_key across the existing
Root, Folder, and File variants, then use an O(1) lookup in result.rotated_nodes
to copy the matching rotated read key. Preserve the existing no-op behavior for
unmatched inode kinds or names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 245635b7-9a71-4992-80a8-eb23ce165905

📥 Commits

Reviewing files that changed from the base of the PR and between 0538822 and 37f0164.

📒 Files selected for processing (43)
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-01-PLAN.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-01-SUMMARY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-02-PLAN.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-02-SUMMARY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-03-PLAN.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-03-SUMMARY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-04-PLAN.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-04-SUMMARY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-05-PLAN.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-05-SUMMARY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-06-PLAN.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-06-SUMMARY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-07-PLAN.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-07-SUMMARY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-PATTERNS.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-RESEARCH.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-SECURITY.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-UAT.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-VALIDATION.md
  • .planning/phases/74-rust-and-fuse-rotation-revocation-soundness/74-VERIFICATION.md
  • .planning/security/REVIEW-2026-07-11-phase74-rotation.md
  • .planning/todos/pending/2026-07-11-remint-trusts-server-recipient-pubkey-binding.md
  • .planning/todos/pending/2026-07-11-ts-rotatednodes-defensive-copy-parity.md
  • crates/api-client/src/client.rs
  • crates/api-client/src/shares.rs
  • crates/fuse/src/lib.rs
  • crates/fuse/src/platform/windows/write_ops.rs
  • crates/fuse/src/test_support.rs
  • crates/fuse/src/write_ops/grant_scope.rs
  • crates/fuse/src/write_ops/implementation/delete.rs
  • crates/fuse/src/write_ops/rotation_deps.rs
  • crates/sdk/src/rotation/engine.rs
  • crates/sdk/src/rotation/mod.rs
  • packages/sdk-core/src/__tests__/rotation/engine.test.ts
  • packages/sdk-core/src/index.ts
  • packages/sdk-core/src/rotation/engine.ts
  • packages/sdk-core/src/rotation/index.ts
  • tests/desktop-e2e/scripts/run-all.ps1
  • tests/desktop-e2e/scripts/run-all.sh
  • tests/desktop-e2e/scripts/shared-scope-exit-rotation.mts
  • tests/desktop-e2e/tsconfig.json

Comment thread .planning/ROADMAP.md
Comment thread crates/fuse/src/platform/windows/write_ops.rs Outdated
Comment thread crates/fuse/src/write_ops/rotation_deps.rs
Comment thread packages/sdk-core/src/rotation/engine.ts
Comment thread tests/desktop-e2e/scripts/shared-scope-exit-rotation.mts
Comment thread tests/desktop-e2e/scripts/shared-scope-exit-rotation.mts
Comment thread packages/sdk-core/src/rotation/engine.ts
FSM1 and others added 2 commits July 11, 2026 13:45
For same-path or case-only renames find_child can return source_ino; the
D-15d destination gate and removal would then delete the source inode
before recreating its name mapping, leaving a dangling mapping / lost
inode. Normalize dest_ino == source_ino to None so it follows the normal
rename/no-op path, mirroring the fuser reference (implementation/rename.rs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 6bf094ec6f4a
…nt fetch caching, e2e stale folderBRef

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 8b04d661e183
Comment thread crates/fuse/src/platform/windows/write_ops.rs
FSM1 and others added 4 commits July 11, 2026 14:04
The rotation engine's re_mint_grants_rooted_at sent the SHARE grant
encryptedReadKey as base64, but the API validates all share
encryptedReadKey fields as even-length hex (update-grant.dto.ts) and
decodes via Buffer.from(.., 'hex'). Base64 400s at runtime and fails the
desktop-e2e SC#2 retained-recipient leg. Switch to hex::encode (internal
node-codec fields stay base64). Update the unit test to hex-decode and
assert the re-minted key matches even-length hex; fix the api-client
update_grant doc comment to say hex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 13bbc5e2f6c8
On the crash-recovery dirty-resume path, repaired descendant read keys
were not surfaced to the caller, so a FUSE/WinFsp inode refresh kept stale
pre-rotation keys (latent revocation-bypass / mis-decrypt hazard).

Rust: repair_dirty_node already inserted each recovered key into
rotated_nodes, but the terminal return discarded the whole map when
fresh_root was None (root already committed on resume). Capture the root's
already-committed post-rotation convenience values on the dirty-resume
branch and return a RotateReadResult carrying the map when any descendant
was repaired.

TS: repairDirtyNode never inserted the recovered readKeyPrime into the
shared rotatedNodes map; add the insert (defensive copy) keyed by the
child ipnsName, mirroring the Rust hook.

Adds a dirty-resume regression test in each engine asserting the repaired
descendant's key is present in the returned map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 9afa56f06d93
…d check

The SC#2 retained-recipient (Carol) check derived her folderB read key
from the pre-rotation folderBRef, whose readKeySealed was sealed under the
OLD grant-root key. The scope-exit rotation re-seals folderB under the NEW
grant-root key, so unwrapping the stale seal with the new key cannot
succeed. Reload folderB's CURRENT SealedChildRef via pollFindChild before
deriveChildReadKey, mirroring the pattern used elsewhere in the file, so
SC#2 genuinely proves retained-vs-revoked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 2a8c73d7a37f
Comment thread crates/fuse/src/platform/windows/write_ops.rs
FSM1 and others added 3 commits July 11, 2026 14:36
A FUSE scope-exit read-key rotation republishes its rotated folders
out-of-band via the rotation engine, so they were never recorded as
locally mutated. A stale in-flight metadata refresh -- one that resolved
a pre-rotation snapshot but completes AFTER the rotation -- then clobbers
the freshly-refreshed inode read keys via apply_owned_children, resetting
them to pre-rotation values. A later parent republish uses the reset key
and reverts the grant-root's child-refs, so the parent's child-ref key
and the child's own record key diverge and every subsequent rotate_one /
verify_subtree_clean fails AES-GCM, failing closed.

This surfaced only on macOS: FUSE-T/SMB's overwrite-rename opens the
SOURCE file, reading the just-rotated node and tripping the divergence;
Linux's kernel rename never opens the source, so the identical
desktop-e2e Part D passed there. Phase 74 SC1 added file rotation, which
made rotated files readable-and-clobberable.

Fix: after refresh_rotated_inode_read_keys, add every rotated folder
inode to fs.mutated_folders. drain_refresh_completions already guards
that set against exactly this folder-state desync -- it runs
mark_remotely_edited_files_unresolved, which PRESERVES keys, instead of
apply_owned_children. A rotation is a local mutation of those folders.

Verified: cargo test -p cipherbox-fuse 117/117; desktop-e2e shared
scope-exit rotation suite 3/3 on macOS FUSE-T with all prior failure
signals -- rotate FAILED / AES-GCM / open EIO -- at zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 917035580c96
…ktop-e2e

The desktop test-login binary holds a single 15m access token for the entire
E2E suite and cannot silently refresh, so on the slowest leg (macOS FUSE-T,
with the 120s+300s macOS-optional sync waits) the token expired mid-run at
login+15m, 401-locking every IPNS resolve/publish and hanging the D-16
shared scope-exit acceptance in Part C. This surfaced only after the Part D
stale-refresh clobber fix let Parts A-C run to completion.

Make ACCESS_TOKEN_TTL env-configurable (production default unchanged at 15m)
and set it to 2h for the desktop-e2e API stack. Logs a follow-up todo for the
underlying product gap: the Rust background client has no auto-refresh on 401.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 4f7e2b3523c0
@FSM1
FSM1 merged commit 2917c85 into main Jul 11, 2026
36 checks passed
@FSM1
FSM1 deleted the feat/rust-and-fuse-rotation-revocation-soundness branch July 11, 2026 17:46
FSM1 added a commit that referenced this pull request Jul 11, 2026
* docs(75): research cross-language IPNS and node-codec verification parity

* docs(phase-75): add validation strategy

* docs(75): add pattern map for cross-language verification parity

* docs(75): create phase plan

Entire-Checkpoint: 28cad9348085

* docs(75): populate validation strategy from research and plans

Entire-Checkpoint: e739f36a0c16

* docs(75): resolve research open questions and annotate roadmap wave deps

Entire-Checkpoint: 0198826aa4cd

* feat(75-01): extend IPNS verify-vector generator with 4 new failing cases

- Parameterize buildCborData to accept Validity string and ValidityType
- Add expired-valid-sig, wrong-validity-type, malformed-rfc3339-trailing-component,
  and malformed-rfc3339-impossible-date cases, each real Ed25519-signed and
  expected_result invalid
- Regenerate tests/vectors/ipns/verify.json to 12 cases; original 8 unchanged

Entire-Checkpoint: e4bebeade437

* docs(75-01): complete extend IPNS verify-vector generator plan

Entire-Checkpoint: 07e263ae724b

* test(75-04): replace fileIv KAT samples with encoding-unambiguous base64

- GCM body vectors (12-byte fileIv) use base64 samples containing a
  non-hex character; CTR body vectors (16-byte fileIv) use base64
  samples with natural == padding — both are invalid as hex
- add expected_file_iv_len_bytes (12 GCM / 16 CTR) per file body vector
- expected_read_body_hex regenerated via the canonical TS codec
  (encodeReadBody), not hand-edited
- seal_vectors[0].fixed_iv and tests/vectors/crypto/node-aad.json left
  untouched (hex-only harness IVs, no base64 code path)

Entire-Checkpoint: 665933e8071a

* feat(75-04): add base64-decode-and-assert-length block to Rust node-codec KAT

- BodyVector gains expected_file_iv_len_bytes: Option<usize> (present only
  on file-kind vectors; folder/root carry no content.fileIv)
- new node_codec_kat_file_iv_is_base64_not_hex test base64-decodes
  content.fileIv and every versions[].fileIv, asserting decoded length
  equals expected_file_iv_len_bytes — a hex decode would fail on the
  non-hex/padded samples from 75-04 Task 1
- existing round-trip / expected_read_body_hex assertions unchanged

Entire-Checkpoint: 02524fcaefa2

* feat(75-04): add base64-decode-and-assert-length it() to TS node-codec KAT

- local base64ToUint8Array mirrors decode.ts's private helper (the
  production decode path), deliberately not the fromHex() helper
- new describe block base64-decodes content.fileIv and every
  versions[].fileIv for every file-kind body vector carrying
  expected_file_iv_len_bytes, asserting decoded length matches — byte-
  identical assertion to the Rust twin (Task 2), driven off the same
  shared VECTORS import
- non-vacuous guard requires at least 2 file body vectors exercised
- existing PRIMARY LOCK assertions unchanged

Entire-Checkpoint: c0a4c666e3cb

* docs(75-04): complete node-codec KAT fileIv encoding lock plan

Entire-Checkpoint: 142c93154f2c

* feat(75-05): add cross-language UUID acceptance oracle

- tests/vectors/crypto/uuid-acceptance.json: shared accept/reject cases
  for canonical-only (Option A) UUID acceptance domain
- covers canonical lower/upper, simple-32-hex, loose-hyphen, braced,
  urn:uuid, non-hex, wrong-length, empty forms

Entire-Checkpoint: 521b2bd35fb0

* test(75-05): add failing canonical-only UUID acceptance tests

- rejects simple-32-hex and loose-hyphen forms (currently accepted by
  the loose strip-then-check uuidToBytes)
- oracle-driven block over tests/vectors/crypto/uuid-acceptance.json
- RED: 3 tests fail against the current implementation

Entire-Checkpoint: ce39c54d2f7b

* feat(75-05): tighten TS uuidToBytes to canonical-only (SC3)

- validate the canonical 8-4-4-4-12 hyphenated shape against the raw
  input before hyphen-stripping, rejecting simple-32-hex and
  loose-hyphen forms that were previously accepted
- GREEN: all 200 @cipherbox/crypto tests pass, including the
  uuid-acceptance.json oracle block

Entire-Checkpoint: 013c621b6186

* test(75-05): add failing cross-language UUID acceptance oracle test

- uuid_acceptance_cross_language drives build_node_aad over every
  uuid-acceptance.json case, asserting Ok for accept / Err for reject
- RED: current Uuid::parse_str over-accepts simple-32-hex, braced,
  and urn:uuid forms that the shared oracle marks reject

Entire-Checkpoint: 44cf5054c3fb

* feat(75-05): add canonical-form pre-check to Rust build_node_aad (SC3)

- is_canonical_uuid_form: dependency-free byte-position check for the
  8-4-4-4-12 hyphenated shape, applied before Uuid::parse_str
- rejects simple-32-hex, braced {...}, and urn:uuid:... forms that
  Uuid::parse_str previously accepted
- no regex/once_cell dependency added to crates/crypto/Cargo.toml
- GREEN: build_node_aad unit tests and cross_language tests pass,
  Rust and TS now produce identical accept/reject verdicts for every
  uuid-acceptance.json case

Entire-Checkpoint: c7ea0bac3b3d

* docs(75-05): complete UUID acceptance-domain parity plan

Entire-Checkpoint: 12fdaeb7318e

* test(75-02): add failing tests for ValidityType decoding

- Extend decode_ipns_cbor_validity's return type to a tuple carrying
  Validity bytes plus an Option<i64> ValidityType placeholder
- Add tests asserting ValidityType 0/1 extraction and duplicate-key
  rejection; extraction not yet implemented so these fail (RED)
- Thread bind_verified's single call site through the new tuple shape
  (ValidityType value not yet consumed)

Entire-Checkpoint: ed32f9cc25f3

* feat(75-02): decode ValidityType in decode_ipns_cbor_validity

- Implement ValidityType extraction alongside Validity bytes, with
  duplicate-key rejection mirroring the existing Validity guard
- Returns Option<i64>: Some(n) when present and an integer, None when
  absent; the == 0 EOL gate is left to the caller (bind_verified,
  Task 2), this decoder only reports the value

Task 1 of 75-02 (Phase 75: cross-language IPNS/node-codec parity).

Entire-Checkpoint: e3dc7037318b

* test(75-02): add failing tests for bind_verified ValidityType gate

- Add tests asserting bind_verified rejects records with a missing or
  non-zero ValidityType, and still accepts/expires ValidityType-0
  records exactly as before
- Missing/non-zero-ValidityType tests fail (RED) since the gate is
  not yet implemented

Entire-Checkpoint: 3897451ec1c1

* feat(75-02): gate bind_verified on ValidityType == 0, widen to pub

- bind_verified now fails closed on missing or non-zero ValidityType
  before treating Validity as an EOL/expiry timestamp (Phase 75
  gap #7 — Rust side; TS mirrors this in Plan 03)
- Widen bind_verified from pub(crate) to pub so crates/fuse's test
  target can call it directly (Task 3 dedups classify_vector onto it)
- parse_rfc3339_to_unix_secs and the 5-minute skew buffer are
  untouched

Task 2 of 75-02.

Entire-Checkpoint: 210682e3dc7b

* test(75-02): dedup classify_vector into a bind_verified wrapper

- Replace classify_vector's hand-spelled cid/sequence binding block
  with a thin wrapper that calls the now-pub cipherbox_api_client
  bind_verified — deletes the duplicated binding logic that let
  gap #9 go undetected (D-07's expiry leg never got mirrored here)
- Bump the non-vacuous vector-count guard 8 -> 12 to match the
  4 new invalid cases landed by Plan 75-01 (expired, wrong-validity-
  type, and two malformed-RFC3339 sub-cases)
- All 12 cases now classify to their expected_result, including the
  4 new cases exercising the Task 2 ValidityType gate end-to-end

Task 3 of 75-02. SC1 (Rust half) met.

Entire-Checkpoint: f9c10ada7067

* docs(75-02): complete Rust ValidityType EOL binding plan

Entire-Checkpoint: bf3c6f7ccd53

* test(75-03): add failing strict RFC3339 + ValidityType assertions

- Bump shared-vector count guard from 8 to 12 (Plan 01's 4 new invalid cases)
- Add 4 per-vector tests for expired-valid-sig, wrong-validity-type,
  malformed-rfc3339-trailing-component, malformed-rfc3339-impossible-date
- Add parseRfc3339ToUnixSecs unit tests mirroring the Rust parser's
  rejection branches (trailing components, impossible dates, non-digit
  fraction, missing Z)
- Add ValidityType absent/non-zero rejection assertions
- RED: fails against the current loose new Date(validityStr) parse and
  unread ValidityType field

Entire-Checkpoint: 86715bc90ed1

* feat(75-03): port strict RFC3339 parser + ValidityType==0 gate to TS

- Add parseRfc3339ToUnixSecs, a manual strict parser mirroring the Rust
  parse_rfc3339_to_unix_secs branch-for-branch (crates/api-client/src/ipns.rs):
  rejects trailing date/time components, impossible calendar dates
  (leap-aware), non-digit fractional seconds, and missing Z, without
  rolling an invalid date forward
- Replace new Date(validityStr).getTime() in resolveIpnsRecord with the
  strict parser; preserve the 5-minute skew buffer and expiry comparison
- Add a fail-closed cborFields['ValidityType'] === 0 gate before treating
  Validity as an expiry timestamp, mirroring Rust's bind_verified
  (T-75-07)
- TS and Rust now reject the same 12-case tests/vectors/ipns/verify.json
  fixture identically (SC1 TS half)

Entire-Checkpoint: 864bd5c6787d

* docs(75-03): complete strict RFC3339 + ValidityType parity plan

Entire-Checkpoint: 5112a3fb234e

* docs(phase-75): complete phase execution

Entire-Checkpoint: 36c3ccaf9bc2

* docs(phase-75): auto-close 4 todos resolved by this phase

Entire-Checkpoint: a2cb16d0454e

* docs(phase-75): evolve PROJECT.md after phase completion

Entire-Checkpoint: 4025885d9fe0

* docs(74): close 3 stranded phase-74 todos resolved by shipped code

Phase 74 (merged PR #607) resolved these but the GSD auto-close under-closed
them, leaving them in pending despite resolves_phase: 74:
- deep-scope-exit rotation now refreshes ALL rotated_nodes inodes (grant_scope.rs)
- WinFsp rename gates the overwritten dest scope-exit (74-06, windows/write_ops.rs)
- query_grants_rooted_at is a real /shares/sent remint, not a no-op (74-07)

Verified each against the merged code + tests before moving; appended a
resolution note to each. No pending resolves_phase:74 todos remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 64ff86dfcd51

* fix(75): close cross-language IPNS verification parity divergences

Phase 75 crypto/privacy review found the Rust and TS IPNS verifiers reach
different verdicts on some signed records. Closed the two material gaps on both
sides so they fail closed identically:

- Duplicate CBOR map keys: TS cborDecode defaulted to last-wins (a signed
  ValidityType:[1,0] decoded to 0 and was accepted) while Rust rejects
  duplicates. TS now decodes with rejectDuplicateMapKeys: true.
- RFC3339 fixed-width fields: Rust parse::<T>() accepted a leading +/- and
  overflowing years; TS did not enforce a 4-digit year. Both now use a
  fixed-width digit check (year 4, others 2), rejecting leading signs and
  non-canonical-width fields identically.

Locked with parser tests on both languages plus a duplicate-map-key integration
test. One LOW benign-direction divergence (CBOR float-0.0 ValidityType) is
deferred to a todo. Full report in .planning/security/REVIEW-2026-07-11-phase75-parity.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 42263fd885f8

* fix(75): harden UUID canonical parity and CodeRabbit review findings

CodeRabbit CLI review of the phase 75 branch surfaced a real SC3 soundness gap
plus test-rigor and bookkeeping items:

- uuidToBytes accepted a trailing-newline UUID because JS `$` (no `m` flag) also
  matches before a final "\n", while Rust is_canonical_uuid_form rejects it
  (bytes.len() != 36) — a cross-language divergence. Added a length === 36 guard
  and a "\n" regression case to the shared uuid-acceptance.json oracle (now run by
  both the TS and Rust oracle tests).
- Tightened the UUID-oracle reject assertions on both sides to require the specific
  INVALID_AAD_INPUT verdict, and made the TS loop fail loudly on an unknown expected
  value instead of silently treating it as a reject.
- Reconciled STATE.md metadata and banner-marked the pre-fix crypto review report so
  the SECURED verdict is unambiguous.

Verified: @cipherbox/crypto 200 passed; cipherbox-crypto cross_language 7 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: e4e60b8860fd

* docs(75): extract phase 75 learnings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 01f9fb37c55b

* fix(75): reject any duplicate CBOR key in Rust IPNS decoders for TS parity

Greptile P1: the TS resolve fix (rejectDuplicateMapKeys: true) rejects a
duplicate of ANY key, but the Rust decoders only rejected duplicates of their
target fields (Value/Sequence, Validity/ValidityType). A signed record with a
duplicate ignored key like TTL was therefore Rust-accepts / TS-rejects — a new
parser-differential in the opposite direction.

decode_ipns_cbor_data and decode_ipns_cbor_validity now reject any duplicate
text map key via a seen-keys set, matching the TS decoder. Verified:
cipherbox-core 28 passed (incl. new dup-TTL test), api-client 25, fuse vectors 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL
Entire-Checkpoint: 1ef91f3a89ac

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:api:feat Minor version bump (new feature) for api release:cipherbox-fuse:feat Minor version bump (new feature) for cipherbox-fuse release:cipherbox-sdk:feat Minor version bump (new feature) for cipherbox-sdk release:desktop:fix Patch version bump (bug fix) for desktop release:sdk:fix Patch version bump (bug fix) for sdk release:sdk-core:feat Minor version bump (new feature) for sdk-core release:tee-worker:fix Patch version bump (bug fix) for tee-worker release:web:fix Patch version bump (bug fix) for web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant